Flylib.com

Books Software

 
 
 

FAQ 24.08 Are there techniques that increase the likelihood that the compiler-synthesized assignment operator will be right?

FAQ 24.08 Are there techniques that increase the likelihood that the compiler- synthesized assignment operator will be right?

Following a few simple rules helps the compiler to synthesize assignment operators that do the right thing. Without an assignment operator discipline, developers will need to provide an explicit assignment operator for too many classes, because the compiler-synthesized version will be incorrect an unnecessarily large percentage of the time.

The following FAQs provide guidelines for an assignment operator discipline that we have found to be effective and practical.

FAQ 24.09 How should the assignment operator in a derived class behave?

An assignment operator in a derived class should call the assignment operator in its direct base classes (to assign those member objects that are declared in the base class), then call the assignment operators of its member objects (to change those member objects that are declared in the derived class). These assignments should normally be in the same order that the base classes and member objects appear in the class's definition. An example follows .

class Base {
public:
  Base& operator= (const Base& b) throw();
protected:
  int i_;
};
Base& Base::operator= (const Base& b) throw()
{
  i_ = b.i_;
  return *this;
}

class Derived : public Base {
public:
  Derived& operator= (const Derived& d) throw();
protected:
  int j_;
};

Derived& Derived::operator= (const Derived& d) throw()
{
  Base::operator= (d);
  j_ = d.j_;
  return *this;
}

Typically, a Derived::operator= shouldn't access the member objects defined in a base class; instead it should call its base class's assignment operator. Nor should a Base::operator= access member objects defined in a derived class (that is, it usually shouldn't call a virtual routine, like copyState() , to copy the derived class's state).

If a Base::operator= tried to copy a derived class's state via a virtual function, the compiler- synthesized assignment operators in the derived classes would be invalidated. This requires defining an explicit assignment operator in an unnecessarily large percentage of the derived classes. This added work often negates any common code that is shared in the base class's assignment operator.

For example, suppose Base defines Base::operator= (const Base& b) , and this assignment operator calls virtual function copyFrom(const Base&) . If the derived class Derived overrides copyFrom(const Base&) to change the entire abstract state of the Derived object, then the compiler-synthesized implementation of Derived::operator= (const Derived&) is likely to be unacceptable: the compiler-synthesized Derived::operator= (const Derived&) would call Base::operator= (const Base&) , which would call back to Derived:: copyFrom(const Base&) ; after returning, the Derived state would be assigned a second time by Derived::operator= (const Derived&) .

At best, this is a waste of CPU cycles because it reassigns the Derived member objects. At worst, this is semantically incorrect, because special changes made during Derived::copyFrom(const Base&) may get wiped out when the Derived member objects are subsequently assigned by Derived::operator= (const Derived&) .

FAQ 24.10 Can an ABC's assignment operator be virtual ?

An ABC's assignment operator can be virtual only if all derived classes of the ABC will be assignment compatible with all other derived classes and if the developer is willing to put up with a bit of extra work. This doesn't happen that often, but here's how to do it.

Classes derived from a base class are assignment compatible if and only if there's an isomorphism between the abstract states of the classes. For example, the abstract class Stack has concrete derived classes StackBasedOnList and StackBasedOnArray . These concrete derived classes have the same abstract state space as well as the same set of services and the same semantics. Thus, any Stack object can, in principle, be assigned to any other Stack object, whether or not they are instances of the same concrete class.

If all classes derived from an ABC are assignment compatible with all other derived classes from that ABC, there are two choices: when a user has a reference to the ABC, either prevent assignment or make it work correctly.

It is easiest on the class implementer to prevent assignment when the user has a reference to the base class. This is done by making the base class's assignment operator protected: . The disadvantage of this approach is that it restricts users from assigning arbitrary pairs of objects referred to by Stack references (that is, by Stack& ).

The other choice is to make assignment work correctly when the user has a reference to the base class. This is done by making the base class's assignment operator public: and virtual . This approach allows any arbitrary Stack& to be assigned with any other Stack& , even if the two Stack objects are of different derived classes. The base class version of the assignment operator must be overridden in each derived class, and these overrides should copy the entire abstract state of the other Stack into the this object.

class Stack {
public:
  virtual ~Stack()                    throw();
  virtual void   push(int elem)       throw() = 0;
  virtual int    pop()                throw() = 0;
  virtual int    getElem(int n) const throw() = 0;
  virtual Stack& operator= (const Stack& s) throw();
protected:
  int n_;
};

Stack::~Stack() throw()
{ }
Stack& Stack::operator= (const Stack& s) throw()
{ n_ = s.n_; return *this; }

void userCode(Stack& s, Stack& s2)
{ s = s2; }

The overridden assignment operator and the overloaded assignment operator in a derived class, such as the StackArray class that follows , are often different.

class StackArray : public Stack {
public:
  StackArray()                       throw();
  virtual void push(int x)           throw();
  virtual int  pop()                 throw();
  virtual int  getElem(int n) const  throw();
  virtual StackArray& operator= (const Stack& s) throw();
                                                //override
  StackArray& operator= (const StackArray& s)    throw();
                                                //overload
protected:
  int data_[10];
};

StackArray::StackArray()             throw()
: Stack() { }
void StackArray::push(int x)         throw()
{ data_[n_++] = x; }
int StackArray::pop()                throw()
{ return data_[--n_]; }
int StackArray::getElem(int n) const throw()
{ return data_[n]; }

// Override:
StackArray& StackArray::operator= (const Stack& s) throw()
{
  Stack::operator= (s);
  for (int i = 0; i < n_; ++i)
    data_[i] = s.getElem(i);
  return *this;
}

// Overload:
StackArray& StackArray::operator= (const StackArray& s) throw()
{
  Stack::operator= (s);
  for (int i = 0; i < n_; ++i)
    data_[i] = s.data_[i];
  return *this;
}

int main()
{
  StackArray s, s2;
  userCode(s, s2);
}

Note that the override ( StackArray::operator= (const Stack&) ) returns a StackArray& rather than a mere Stack& . This is called a covariant return type .