| [ Team LiB ] |
|
Gotcha #54: Copy Constructor Base InitializationHere are a couple of simple components:
class M {
public:
M();
M( const M & );
~M();
M &operator =( const M & );
// . . .
};
class B {
public:
virtual ~B();
protected:
B();
B( const B & );
B &operator =( const B & );
// . . .
};
Let's leverage these components to produce a new class—and try to get the compiler to do as much work for us as is reasonable:
class D : public B {
M m_;
};
While class D doesn't inherit constructors, destructor, or the copy assignment operator from its base class, the compiler will write these operations for us implicitly, leveraging the corresponding implementations of the components. (See Gotcha #49.) For example, the compiler's implementation of D's default constructor will be as a public inline member function. The constructor will first invoke the base class B's default constructor, then the default constructor for the M member. The destructor will, as always, do the inverse: it will first destroy the member, then call the base class destructor. The copy operations are more interesting. The compiler-generated copy constructor will perform a member-by-member initialization, as if we had written it like this:
D::D( const D &init )
: B( init ), m_( init.m_ )
{}
The compiler-generated assignment operator performs a member-by-member assignment, as if we had written it like this:
D &D::operator =( const D &that ) {
B::operator =( that );
m_ = that.m_;
return *this;
}
Suppose we add a data member to our class that doesn't define these operations? For example, we could add a data member that points to a heap-allocated X:
class D : public B {
public:
D();
~D();
D( const D & );
D &operator =( const D & );
private:
M m_;
X *xp_; // new data member
};
Now we should write all these operations explicitly. The default constructor and the destructor are straightforward, and we can let the compiler do most of the work for us:
D::D()
: xp_( new X )
{}
D::~D()
{ delete xp_; }
The compiler invokes the default constructors and destructors for the base class and member m_ implicitly. It's tempting to think we can get away with the same approach when implementing copy construction and copy assignment, but we can't: D::D( const D &init ) : xp_( new X(*init.xp_) ) {} D &D::operator =( const D &rhs ) { delete xp_; xp_ = new X(*rhs.xp_); return *this; } Both these implementations will compile without error and do the wrong thing at runtime. Our copy constructor implementation correctly initializes the member xp_ with a copy of what its initializer's xp_ refers to, but the base class and m_ member are initialized using B's and M's default constructors respectively, rather than their copy constructors. In the case of the assignment, the values of the base class part and m_ are unchanged. Once you take over the job of writing any of these member functions from the compiler, you're responsible for the entire implementation:
D::D( const D &init )
: B( init ), m_( init.m_ ), xp_( new X(*init.xp_) )
{}
D &D::operator =( const D &rhs ) {
if( this != &rhs ) {
B::operator =( rhs );
m_ = rhs.m_;
delete xp_;
xp_ = new X(*rhs.xp_);
}
return *this;
}
This is the case for the default constructor and destructor as well, but the implicit invocation of the default constructors for the base class and m_ member resulted in correct code in that case. I prefer the approach that minimizes typing, but if you prefer, you can be explicit:
D::D()
: B(), m_(), xp_( new X )
{}
|
| [ Team LiB ] |
|