[ Team LiB ] |
![]() ![]() |
Gotcha #47: Assignment/Initialization ConfusionTechnically, assignment has little to do with initialization. They're separate operations, used in different circumstances. Initialization is the process of turning raw storage into an object. For a class object, this could entail setting up internal mechanisms for virtual functions and virtual base classes, runtime type information, and other type-dependent information (see Gotchas #53 and #78). Assignment is the process of replacing the existing state of a well-defined object with a new state. Assignment doesn't affect internal mechanisms that implement type-dependent behavior of an object. Assignment is never performed on raw storage. Idiomatically, however, if copy construction semantics are important in one set of circumstances, chances are that copy assignment semantics are important in the others, and vice versa. Forgetting to consider both assignment and initialization will result in bugs: class SloppyCopy { public: SloppyCopy &operator =( const SloppyCopy & ); // Note: compiler default SloppyCopy(const SloppyCopy &) . . . private: T *ptr; }; void f( SloppyCopy ); // pass by value SloppyCopy sc; f( sc ); // alias what ptr points to, probable error Argument passing is accomplished with initialization, not assignment; the formal argument to f is initialized by the sc actual argument. The initialization will be accomplished with SloppyCopy's copy constructor. In the absence of an explicitly declared copy constructor, the compiler will write one. In this case, the compiler's version will be incorrect. (See Gotchas #49 and #53.) The idiomatic assumption is that, even though copy construction and copy assignment are different operations, they should have similar, or conformant, meaning: extern T a, b; b = a; T c( a ); In the code above, users of the type T would expect the values of b and c to be conformant. In other words, it should be immaterial to subsequent execution whether an object of type T received its current value as the result of an assignment or an initialization. This assumption of conformance is so ingrained in the C++ programming community that the standard library depends on it:
template <class Out, class T> class raw_storage_iterator : public iterator<output_iterator_tag,void,void,void,void> { public: raw_storage_iterator& operator =( const T& element ); // . . . protected: Out cur_; }; template <class Out, class T> raw_storage_iterator<Out, T> & raw_storage_iterator<Out,T>::operator =( const T &val ) { T *elem = &*cur_; // get a ptr to element new ( elem ) T(val); // placement and copy constructor return *this; } A raw_storage_iterator is used to assign to uninitialized storage. Ordinarily, an assignment operator requires that both its arguments be properly initialized objects; otherwise, a problem is likely when the assignment attempts to "clean up" the left argument before setting its new value. For example, if the objects being assigned contain a pointer to a heap-allocated buffer, the assignment will typically delete the buffer before setting the new value of the object. If the object is uninitialized, the deletion of the uninitialized pointer member will result in undefined behavior:
struct X { T *t_; X &operator =( const X &rhs ) { if( this != &rhs ) { delete t_; t_ = new T(*rhs.t_); } return *this; } // . . . }; // . . . X x; X *buf = (X *)malloc( sizeof(X) ); // raw storage . . . X &rx = *buf; // foul trickery . . . rx = x; // probable error! The copy algorithm from the standard library copies an input sequence to an output sequence, using assignment to perform the copy of each element: template <class In, class Out> Out std::copy( In b, In e, Out r ) { while( b != e ) *r++ = *b++; // assign src element to dest element return r; } Use of copy on an uninitialized array of X will most probably fail:
X a[N];
X *ary = (X *)malloc( N*sizeof(X) );
copy( a, a+N, ary ); // assign to raw storage!
Assignment is a bit like (but not exactly like!) a destruction followed by a copy construction. The raw_storage_iterator allows assignment to uninitialized storage by reinterpreting the assignment as a copy construction, skipping the problematic "destruction" step. This will work only under the assumption that copy assignment and copy construction produce acceptably similar results.
raw_storage_iterator<X *, X> ri( ary ); copy( a, a+N, ri ); // works! This is not to imply that the designer of class X must be intimately aware of all the (admittedly difficult and obscure) details of the standard library to produce a correct implementation. However, the designer does have to be aware of the general, idiomatic assumption that copy initialization and copy assignment are conformant. An abstract data type that doesn't support this conformance can't be leveraged effectively with the standard library and will be less useful than a type that does conform. Another common misapprehension is that assignment is somehow involved in the following initialization: T d = a; // not an assignment That = symbol is not an assignment operator, and d is initialized by a. This is fortunate, since otherwise we'd have an assignment to uninitialized storage. (But see Gotcha #56.) ![]() |
[ Team LiB ] |
![]() ![]() |