C/C++

Implementing Splay Trees in C++

Splay trees are self-adjusting binary search trees that are typically used in caches, memory allocators, routers, garbage collectors, data compression, and the like.

The Standard Library defines several operations on containers. This makes implementing look-alike associative containers a lot of work. Fortunately, you can implement some of the operations with the help of others. This means that after implementing the most basic operations, others can use these to implement their functionality. For example, the insert method taking an iterator range as arguments can use the single element insert as its working horse; see Listing One.

Listing One

The Standard comes to the rescue by defining the semantics for some of the operations in terms of other operations. For example, the method equal_range is defined in terms of lower_bound and upper_bound. All in all, most of the implementation work is done after implementing the tree manipulation, iterators, and basic versions of insert and erase.

Many books on binary search trees suggest implementing the tree manipulation using recursion. This seems obvious because of the recursive nature of the binary tree structure (Listing Two). However, it isn't really a good idea. Programming languages like C++ allow recursion, but they aren't built for huge recursion depth. Huge recursion depth is exactly what you get when manipulating large binary search trees. For C++ implementations, a better approach is to base the tree manipulation on an iterative implementation; see Listing Three.

Implementing tree handling with loops instead of recursion doesn't do anything good for the readability of the code. On the other hand, it makes it possible to manipulate huge search trees, and is faster.

Code and Data Layout

Like all template classes, my splay-tree implementation has the potential of generating huge amounts of code if used with different types, especially if the compiler does aggressive inlining. To fight this, I split my implementation class and node class in twoa base class containing all the code not dependent on the template parameters, and a derived template class containing the rest of the code (Figure 5). This is a well-known idiom, but won't necessarily fight code bloat. It does, however, give the compiler an opportunity to share code between instantiations if it optimizes for minimal space. And while splitting the implementation into two classes doesn't improve the readability of the code, it seems to be a necessity if the implementation has to be used in serious projects. For the node class, this trick works quite well. Almost all code is moved to the base class; only a constructor remains. All nodes in the tree are accessed through pointers to node_base. If it is necessary to retrieve the element in the node, a static_cast from pointer to node_base to pointer to node is done. This is safe because all nodes are of type node, even though they are pointed to by node_base pointers. As an extra benefit, it's fast because the static_cast probably won't generate any code (the two pointers probably have the same value).

Figure 5: Division of classes into template and nontemplate part.

The trick didn't work as well for the splay_tree class. I had originally planned to put some of the code that didn't depend on the element type (such as the method size and the variable size_, which holds the number of elements) in the base class. Instead, it went into the template class. This was necessary because they depend on the type size_type, defined as an alias to the type size_type in the allocator, thereby indirectly depending on the element type. For other functions, I placed some of the tree manipulation code in splay_tree_base, but not as much as I had hoped for. The reason for this is that code dependent on the template parameter is interleaved with code that is not dependent, making it almost impossible to untangle the code in two parts without changing the semantics.

It is possible to implement splay trees with only an element and two pointers to child nodes in each node. Such an implementation would use top-down splaying. In my implementation, the nodes hold a parent pointer to make bottom-up splay work, and to facilitate iterators.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!