Object Swapping Part 1: Its Surprising Importance
March 14, 2012
If you're like most C++ programmers, you probably learned about copy constructors and copy-assignment operators as part of learning how to define your own classes; and not until much later — if at all — did you learn about how to define a useful swap operation for your class. I am beginning to believe that this strategy is a mistake — that swapping is as fundamental an operation as copying, and that it may even be more fundamental than assignment.
To see why, let's start by considering an analogous pair of operations: + and +=. If we are defining a class
T
, the naïve approach is to define + first, and then define += in terms of it:1 2 3 4 5 6 7 8 9 10 11 12 | // Naïve class T { public : // … friend T operator+( const T&, const T&); // += defined in terms of + T& operator+=( const T& t) { * this = * this + t; return * this ; } }; |
The precise definition of
operator+
doesn't matter here; what matters is that operator+
creates a new object of type T
that depends in some way on the contents of its two arguments.There's nothing outright wrong with this strategy, but it has a drawback: Every time we execute +=, we create a new object, assign that object to the left-hand side of +=, and then throw it away. This extra object gets created and destroyed even if there is a way to do the computation implied by += without that object. By definition, + has to create a new object, but += doesn't — so the way to solve this problem is to turn it around and define + in terms of +=:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Less naïve class T { public : // … T& operator+=( const T& t); }; // + defined in terms of += T operator+( const T& a, const T& b) { T result = a; result += b; return result; } |
Here I have omitted the definition of +=, just as earlier I omitted the definition of +. By refactoring the code in this way, I have caused += to do just the essential work, and then wrapped += in a function that constructs a new object, uses += to augment that object, and then returns the object.
Similar, but less obvious, reasoning applies to the relationship between copying, swapping, and assignment. If we want to implement swapping in terms of copying and assignment, we might do so this way:
1 2 3 4 5 6 7 8 9 10 | // Naïve class T { public : // … // Swap defined in terms of copy and assignment void swap(T& t) { T temp = t; t = * this ;
* this = temp; }; }; |
Alternatively, if we have copying and swapping available to us, we can implement assignment this way:
1 2 3 4 5 6 7 8 9 | // Less naïve class T { public : // … // Assignment defined in terms of copy and swap T& operator=( const T& t) {
T temp = t; this ->swap(temp);
return * this ; } }; |
Here, the statement
1 | T temp = t; |
is, of course, a copy-initialization, not an assignment; so there is no recursion loop.
It has taken me a long time to realize that this second approach is superior to the first in virtually every way except its subtlety. For example, it is a generally desirable property of operations that might throw an exception that they complete either all of the requested operation or none of it. This definition of
operator=
does not have that property, for if the statement1 | * this = temp; |
in the first example throws an exception, the value of
t
has already been overwritten. As a result, the swap
operation has changed one but not both of its operands before throwing an exception — even if assignment
and copy
have the desirable property of doing everything or nothing. In contrast, if swapping has the all-or-nothing property, then so does the operator=
that we have defined in terms of it.As another example, for many data structures it is possible to define swapping in a way that is much faster than copying or assignment, because swapping normally does not require replicating the data in the data structure. Therefore, defining swapping in terms of copying and assignment has the potential of wasting enormous amounts of time, whereas defining assignment in terms of copying and swapping avoids that waste.
The fundamental point is that like +, assignment combines operations. In the case of +, there were two operations, namely creating a new object and combining a second object with it. In the case of assignment, there were three operations: creating a copy of the right-hand side, destroying the old value of the left-hand side, and storing the copy as the new value of the left-hand side.
In both cases, we are better off implementing the operations separately and combining them as needed. In the case of +, we constructed a temporary object and used += to combine it with another; in the case of assignment, we used the copy constructor to construct a temporary object as a copy of the right-hand side, swapped that copy with the left-hand side, and finally let the temporary object's destructor get rid of it.
Next week, I'll show how swapping instead of assigning can make some algorithms dramatically faster.