Feb. 9, 2009, 10:09 a.m.
posted by nogood
Item 13. Copy OperationsCopy construction and copy assignment are different operations. Technically, they have nothing to do with each other, but socially they hang out together and must be compatible.
class Impl;
class Handle {
public:
//...
Handle( const Handle & ); // copy constructor
Handle &operator =( const Handle & ); // copy assignment
void swap( Handle & );
//...
private:
Impl *impl_; // pointer to implementation of Handle
};
Copying is such a pervasive operation that it's even more important than usual to follow convention. These operations are always declared as a pair, with the signatures above (but see auto_ptr Is Unusual [43, 147] and Preventing Copying [32, 111]). That is, for a class X, the copy constructor should be declared X(const X &), and the copy assignment operator should be declared X &operator =(const X &). It's common and often a good idea to define a member swap function if a member implementation of swap has a performance or exception safety advantage over a traditional non-member swap. An implementation of a typical non-member swap is straightforward:
template <typename T>
void swap( T &a, T &b ) {
T temp(a); // T's copy ctor
a = b; // T's copy assignment
b = temp; // T's copy assignment
}
This swap (identical to the standard library swap) is defined in terms of the type T's copy operations, and it works well if T's implementation is small and simple but may be expensive otherwise. We can do better for a class such as Handle by just swapping the pointer to its implementation.
inline void Handle::swap( Handle &that )
{ std::swap( impl_, that.impl_ ); }
Remember the old comedy routine in which we're told how to get a million dollars and never pay any taxes on it? First, you get a million dollars.... In a similar vein, we can show how to write an exception safe copy assignment operation. First, you get an exception safe copy constructor and an exception safe swap operation. The rest is easy:
Handle &Handle::operator =( const Handle &that ) {
Handle temp( that ); // exception safe copy construction
swap( temp ); // exception safe swap
return *this; // we assume temp's destruction won't throw
}
This technique works particularly well for "handle" classes, that is, classes that consist mostly or entirely of a pointer to their implementations. As we saw in an earlier example in this item, writing exception safe swaps for such classes is both trivial and efficient. The subtle point of this implementation of copy assignment is that the behavior of copy construction must be compatible with that of copy assignment; they're different operations, but there is a pervasive, idiomatic assumption that they will produce indistinguishable results. That is, whether one writes Handle a = ... Handle b; b = a; // assign a to b or Handle a = ... Handle b( a ); // initialize b with a the resulting value and future behavior of b should be indistinguishable whether it received that value through an assignment or an initialization. This compatibility is particularly important when using the standard containers, because their implementations will often substitute copy construction for copy assignment, with the expectation that either operation will produce identical results (see Placement New [35, 119]). A perhaps more common implementation of copy assignment has the following structure:
Handle &Handle::operator =( const Handle &that ) {
if( this != &that ) {
// do assignment...
}
return *this;
}
It's often necessary for correctness, and occasionally more efficient, to perform a check for assignment to self, that is, ensure the left side (this) and right side (that) of the assignment have different addresses. At one time or another in their careers, most C++ programmers toy with the idea of implementing virtual copy assignment. It's legal but subtly complex, so don't do it. Clone instead (see Virtual Constructors and Prototype [29, 99]). |
- Comment