More and more, I bump into people who, by default, want to implement move assignment in terms of swap. This disturbs me, because (1) it's often a pessimization in a context where optimization is important, and (2) it has some unpleasant behavioral implications as regards resource management.
Let's consider a simplified case of a container that contains a pointer to its contents, which are stored on the heap. I'm using a raw pointer, because I don't want to abstract anything away through the use of smart pointers.
class Container {
public:
Container& operator=(Container&& rhs);
private:
int *pData; // assume points to an array
};
Implementing the move assignment operator using std::swap, the code looks like this:
Container& Container::operator=(Container&& rhs)
{
std::swap(pData, rhs.pData);
return *this;
}
Swapping two pointers calls for three pointer assignments,
template<typename T>
void swap(T& lhs, T& rhs)
{
auto temp(lhs);
lhs = std::move(rhs);
rhs = std::move(temp);
}
so the cost of implementing move assignment using swap is three pointer assignments.
However, if we assume that an empty container has a null pData pointer, move assignment can be implemented using only 2 pointer assignments:
Container& Container::operator=(Container&& rhs)
{
delete [] pData;
pData = rhs.pData;
rhs.pData = nullptr;
return *this;
}
I'm ignoring the delete for now, but we'll get back to it later. At this point, I want to observe that non-swap move assignment performs only 2/3rds the assignments of its swap-based cousin. That's important, because move operations should typically be as efficient as possible. Remember that they're optimizations of copy operations, and if you're not concerned about their efficiency, why not just omit them and let rvalues be copied? My feeling is that the fact that a class author went to the trouble of adding support for move operations is a sign that the author perceives the class to be one where speed is important. If that's the case, it seems unreasonable to pay a premium to put the object being moved from into the state of the target of the assignment when it's cheaper to put the object into a different, but equally valid (typically default-constructed), state. After all, the semantics of move assignment typically don't specify the state of the source object of the move after the assignment has been performed, so if callers can't rely on it having the state of the target object before the assignment, why pay for it?
But back to the delete. Regardless of which move assignment implementation is used, the delete will eventually be performed. If it doesn't take place in the move assignment operator for the object that's the target of the move assignment, it'll probably occur in the destructor of the object that's the source of the move assignment. The cost therefore doesn't vary between the implementations, but
when you incur that cost does, and that can be important.
Suppose that Container objects typically use a lot of memory--enough that you have to worry about it. Now consider the following scenario:
{
Container c1, c2;
...
c1 = std::move(c2);
...
}
In the non-swap implementation of move assignment, c1's memory is released at the point of the assignment, but in the swap version, that memory becomes associated with c2, and the memory may not be released until the end of the scope. That might well surprise the caller, who could hardly be blamed for thinking that when an assignment was made to c1, c1's old resources would be released. And it could certainly increase the maximum amount of memory used by the application at any given time.
In my experience, the impact of move-assignment-by-swap on the timing of resource release isn't as well known as it should be, even though the issue was well described many years ago (e.g., Thomas Becker
here and David Abrahams
here).
Now, bear in mind that I said at the outset that I was disturbed by people who,
by default, want to implement move assignment in terms of swap. I have no issue with developers who, consciously aware of the performance and behavioral implications of move-assignment-via-swap, choose to use it anyway. For some types, it may be a perfectly valid implementation choice. My concern is that it's gaining a reputation as
the way to implement move assignment, and I don't think that's good for C++.
Am I mis-analyzing the situation?
Scott