I l@ve RuBoard | ![]() ![]() |
Solution![]() This idiom[3] is frequently recommended, and it appears as an example in the C++ standard (see the discussion in the accompanying box). It is also exceedingly poor form and causes no end of problems. Don't do it.
Fortunately, as we'll see, there is a right way to get the intended effect.
Now let's look at the questions again.
This idiom expresses copy assignment in terms of copy construction. That is, it's trying to make sure that T's copy assignment and its copy constructor do the same thing, which keeps the programmer from having to needlessly repeat the same code in two places. This is a noble goal. After all, it makes programming easier when you don't have to write the same thing twice, and if T changes (for example, gets a new member variable) you can't forget to update one of the functions when you update the other. This idiom could be particularly useful when there are virtual base classes that have data members, which would otherwise be assigned incorrectly at worst or multiple times at best. While this sounds good, it's not really much of a benefit in reality, because virtual base classes shouldn't have data members anyway (see Meyers98, Meyers99, and Barton94). Also, if there are virtual base classes, it means that this class is designed for inheritance, which (as we're about to see) means we can't use this idiom—it is too dangerous. The rest of the first question was: Correct any coding flaws in the version above. The above code has one flaw that can be corrected, and several others that cannot. Problem 1: It Can Slice ObjectsThe code "this->~T(); new (this) T(other);" does the wrong thing if T is a base class with a virtual destructor. When called on an object of a derived class, it will destroy the derived object and replace it with a T object. This will almost certainly break any subsequent code that tries to use the object. See Item 40 for further discussion of slicing. In particular, this makes life a royal pain for authors of derived classes (and there are other potential traps for derived classes; see below). Recall that derived assignment operators are usually written in terms of the base's assignment, as follows: Derived& Derived::operator=( const Derived& other ) { Base::operator=( other ); // ...now assign Derived members here... return *this; } In this case, we get: class U : T { /* ... */ }; U& U::operator=( const U& other ) { T::operator=( other ); // ...now assign U members here... // ...oops, but we're not a U any more! return *this; // likewise oops } As written, the call to T::operator=() silently breaks all the code after it (both the U member assignments and the return statement). This often manifests as a mysterious and hard-to-debug run-time error if the U destructor doesn't reset its data members to invalid values. To correct this problem, we could try a couple of alternatives.
Alas, both of these patchwork solutions only replace an obvious danger with a subtler one that can still affect authors of derived classes (see below). Guideline
Guideline
Now for the second question: Even with any flaws corrected, is this idiom safe? Explain. No. Note that none of the following problems can be fixed without giving up on the entire idiom. Problem 2: It's Not Exception-SafeThe new statement will invoke the T copy constructor. If that constructor can throw (and many classes do report constructor errors by throwing an exception), then the function is not exception-safe, because it will end up destroying the old object without replacing it with anything. Like slicing, this flaw will break any subsequent code that tries to use the object. Worse, it will probably cause the program to attempt to destroy the same object twice, because the outside code has no way of knowing that the destructor for this object has already been run. (See Item 40 for further discussion of double destruction.) Guideline
Problem 3: It Changes Normal Object LifetimesThis idiom breaks any code that relies on normal object lifetimes. In particular, it breaks or interferes with all classes that use the common "resource acquisition is initialization" idiom. In general, it breaks or interferes with any class whose constructor or destructor has side effects. For example, what if T (or any base class of T) acquires a mutex lock or starts a database transaction in its constructor and frees the lock or transaction in its destructor? Then the lock/transaction will be incorrectly released and reacquired during an assignment—typically breaking both client code and the class itself. Besides T and T's base classes, this can also break T's derived classes if they rely on T's normal lifetime semantics. Some will say, "But I'd never do this in a class that acquires/releases a mutex in its constructor/destructor!" The short answer is, "Really? And how do you know that none of your (direct or indirect) base classes does so?" Frankly, you often have no way of knowing this, and you should never rely on your base classes working properly in the face of playing unusual games with object lifetimes. The fundamental problem is that this idiom subverts the meaning of construction and destruction. Construction and destruction correspond exactly to the beginning/end of an object's lifetime, at which times the object typically acquires/releases resources. Construction and destruction are not meant to be used to change an object's value (and in fact they do not; they actually destroy the old object and replace it with a lookalike that happens to carry the new value, which is not the same thing at all). Problem 4: It Can Still Break Derived ClassesWith Problem 1 solved using the first approach, namely calling "this->T::~T();" instead, this idiom replaces only the "T part" (or T base subobject) within a derived object. For one thing, this should make us nervous because it subverts C++'s usual guarantee that base subobject lifetimes embrace the complete derived object's lifetime—that base subobjects are always constructed before, and destroyed after, the complete derived object. In practice, many derived classes may react well to having their objects' base parts swapped out and in like this, but some may not. In particular, any derived class that takes responsibility for its base class's state could fail if its base parts are modified without its knowledge (and invisibly destroying and reconstructing an object certainly counts as modification). This danger can be mitigated as long as the assignment doesn't do anything more extraordinary or unexpected than a "normally written" assignment operator would do. Problem 5: this != &otherThe idiom relies completely on the this != &other test. (If you doubt that, consider what happens in the case of self-assignment if the test isn't performed.) The problem is the same as the one we covered in Item 38: Any copy assignment that is written in such a way that it must check for self-assignment is probably not strongly exception-safe.[4]
This idiom harbors other potential hazards that can affect client code and/or derived classes (such as behavior in the presence of virtual assignment operators, which are always a bit tricky at the best of times), but this should be enough to demonstrate that the idiom has some serious problems. And for the last part of the second question: If not, how else should the programmer achieve the intended results? Expressing copy assignment in terms of copy construction is a good idea, but the right way to do it is to use the canonical (not to mention elegant) form of strongly exception-safe copy assignment. Provide a Swap() member that is guaranteed not to throw and that simply swaps the guts of this object with another one as an atomic operation, then write the copy assignment operator as follows: T& T::operator=( const T& other ) { T temp( other ); // do all the work off to the side Swap( temp ); // then "commit" the work using only return *this; // nonthrowing operations } This method still implements copy assignment in terms of copy construction, but it does it the right, safe, low-calorie, and wholesome way. It doesn't slice objects; it's strongly exception-safe; it doesn't play games with normal object lifetimes; and it doesn't rely on checks for self-assignment. For more about this canonical form and the different levels of exception-safety, see also Items 8 through 17, and 40. So, in conclusion, the original idiom is full of pitfalls; it's often wrong; and it makes life a royal pain for the authors of derived classes. I'm sometimes tempted to post the problem code in the office kitchen with the caption: "Here be dragons."[5]
Guideline
Guideline
![]() |
I l@ve RuBoard | ![]() ![]() |