Introduction
The Standard Template Library (STL) adds a lot of fundamental functionality to C++. One of its most prominent features are containers. Containers can be used to store any kind of objects. Various different containers are available for the different requirements a developer might have. Some of the containers are optimized for random access, while others are very efficient when it comes to sorting objects.
To be able to sort objects, the STL containers (and functions) make use of comparators and/or an object’s <-operator. That way it becomes quite easy for developers to create classes which can be stored in a container. But there are a couple of requirements for these comparators, as this paper lays out.
Strict Weak Ordering
Let’s assume we have a simple class called “Car”:
1 2 3 4 5 6 7 8 9 10 11 |
class Car { // member variables private: int m_Color; int m_Type; // constructors public: Car(int color, int type) : m_Color(color), m_Type(type) { } }; |
Next we define a <-operator for our “Car”-class by sorting it by its color and its type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Car { // member variables private: int m_Color; int m_Type; // constructors public: Car(int color, int type) : m_Color(color), m_Type(type) { } // operators public: bool operator<(const Car& operand2) const { return m_Color < operand2.m_Color || m_Type < operand2.m_Type; } }; |
Now we create 2 instances of the class:
1 2 |
Car car1(1, 1); Car car2(1, 2); |
If we’d call: bool bsmaller = car1 < car2; // bsmaller = true the result would be as expected (since car1.m_Type < car2.m_Type).
Now let’s put these cars in a set:
1 2 3 |
std::set<Car> Carpark; Carpark.insert(car1); Carpark.insert(car2); |
So far, so good. We have a container with two cars, so what? — Let’s put another one into the container and see what happens:
1 2 |
Car car3(2, 1); Carpark.insert(car3); |
Outch… That results in a runtime error at best, or undefines behavior at worst.
<-operator requirements
What went wrong?
Well, the problem lies within our defined <-operator and the fact that the set-container uses it to try to put our cars into an order. If we compare car2 with car3, we get contradictory results:
1 2 |
bool compare1 = car2 < car3; // compare1 = true: 1 < 2 || 2 < 1; bool compare2 = car3 < car2; // compare2 = true: 2 < 1 || 1 < 2; |
Therefore, the set doesn’t know how to sort these objects in its internal red/black-tree.
For most of the STL functions/template classes which require a comparator, a so called strict weak ordering comparator is required. Such a comparator is defined by fulfilling the following requirements:
- the <-operator imposes an order:
if (a < b) then !(b < a) - an object is never smaller than itself (i.e. it can’t be ordered before itself):
a < a = false - the <-operator can be used to check objects for equality:
if (!(a < b)) && !(b < a)) then a == b - the ordering is transitive:
if (a < b) && (b < c) then (a < c)
So one might come to the following great solution to the problem and say: “Let’s sort objects by their memory address!”
1 2 3 4 |
bool operator<(const Car& operand2) const { return this < &operand2; } |
Nice idea. That comparator meets all the above given requirements, since an object’s address is unique, if run on a single PC (at no time two objects can occupy the same memory address) plus this idea has the advantage that no additional memory (for instance for a unique identifier used to order the objects) is required.
As long as there is no special requirement to keep objects sorted in a special order within a container this can be a feasible solution. However, it’s not completely safe under all circumstances, as the following chapter will uncover.
Copy Constructor and =-operator
Some of the STL functions/containers make use of an object’s assignment-operator or its copy constructor. For instance there is a function called make_heap(). That function creates a copy of the first object and in addition uses the assignment operator of the class of the contained objects to swap objects. That way a heap is created. So why is this problematic?
Well, the functions are designed under the following assumption:
The <-operator compares objects based on their content AND neither the copy constructor nor the assignment operator alter the object’s order.
Given as a general example, the assertion in the following code is expected to be true: if (a < b) { c = a; a = b; b = c; assert(b < a); }
If we use the object’s address within our <-operator, that’s no longer true. Assume a and b have the following addresses: a = 0x1; b = 0x2;
To make it easier to see the problem further assume that each object stores one integer: a.i = 1; b.i = 2;
Before the swap, a is considered smaller than b (since 0x1 < 0x2). Now we swap the objects: c = a; a = b; b = c;
As you see, the objects changed their content: a.i = 2; b.i = 1; but switching should not have an impact on the comparator; hence, assert(b < a) should be true since b now contains the content of a and a contains the content of b, but it isn’t!
Remember, we wrote the <-operator to compare the objects based on their addresses — and these haven’t changed — so a is still smaller than b (since 0x1 < 0x2).
So we changed the order of the objects and the STL functions don’t know what to do about it (resulting in an error or undefined behavior). We need another way to come up with an implementation for our operator.
Comperator Template
Though our initial <-operator meets the first two requirements, it lacks transitivity and therefore can’t be used to put objects into a unique order. We can correct this, by using the following template to write a comparator which sorts an object based on comparing multiple member variables:
For an object of class “A” with “n” member variables m[0, n) where each member variable type provides a <-operator:
1 2 3 4 5 6 7 8 |
bool operator<(const A& operand2) const { return (m[0] < operand2.m[0]) || !(operand2.m[0] < m[0]) && ((m[1] < operand2.m[1]) || !(operand2.m[1] < m[1]) && ((m[2] < operand2.m[2]) || [...] !(operand2.m[n-2] < m[n-2]) && (m[n-1] < operand2.m[n-1])[...]))); } |
The function compares the object’s first member variable. If the current object’s first member variable is less than the second object’s one, it returns true. If it isn’t, it checks both member variables for equality by making use of the second requirement for <-operators (if (!(a < b)) && !(b < a)) then a == b).
Due to the order of the parentheses in the expression, only in case that the first member variable is equal, it compares the second member variable and returns true, if the first one’s is smaller than the second one’s. The procedure is then repeated for all remaining member variables.
Applying that template to our “Car”-class, would result in the following operator:
1 2 3 4 5 |
bool operator<(const Car& operand2) const { return (m_Color < operand2.m_Color) || !(operand2.m_Color < m_Color) && (m_Type < operand2.m_Type)); } |
That’s it. We now have a working strict weak ordering operator.
Conclusion
Writing a <-operator helps a lot to more conveniently work with STL-containers. However, the developer has to be aware of the additional requirements for the implementation and must be careful to make sure that these requirements are met. Failure to do so can easily result in bugs introduced into the code which are really hard to trace down, since they can occur randomly and not all of the logical errors of an <-operator can be traced down by additional checks within the STL implementation.
Nevertheless, having a properly written <-operator at hand is the basis to make use of most of the STL-functions and improves productivity as well as increases code maintainability.
References
[1] S. Kuhlins, M. Schader, 2005. Die C++ Standardbibliothek. 4th ed. Berlin, Heidelberg, New York: Springer. Ch.1.3.
[2] Accredited Standards Committee WG21/N1043, 1996, Working Paper for Draft Proposed International Standard for Information Systems–Programming Language C++. [internet] Available at: http://www.open-std.org/jtc1/sc22/open/n2356/
[Accessed 23 February 2009]. Ch.23.1.2.
[3] P. J. Plauger, A. Stepanov, M. Lee, D. R. Musser, 2001. The C++ Standard Template Library. ed. Upper Saddle River: Prentice-Hall p.134