The introduction of virtual function mechanism complicates the implementation of assignment operators. Practical experience 73 describes the traps of common overloaded assignment operators.
Virtual assignment
Declare the overloaded assignment operator implementation as a virtual function, called virtual assignment. It is legal for assignment operators to declare virtual functions, but virtual assignments are unreasonable.
Let's start this discussion by looking at a typical example of virtual assignment and using this example as an introduction. This code can be compiled, but it has an obvious problem assigning objects of type CTCPStream to objects of type CUDPStream. However, due to the introduction of virtual assignment, these cannot be avoided. The code is as follows:
// Traffic implementation base class class CStreamBase { public: virtual CStreamBase& operator = (const CStreamBase& stream) = 0; }; // TCP Traffic class CTCPStream: public CStreamBase { public: virtual CStreamBase& operator = (const CStreamBase& stream) { // There are risks. CTCPStream tcpstream = static_cast<CStreamBase>(stream); ..... return *this; } }; // UDP Traffic class CUDPStream: public CStreamBase { public: virtual CStreamBase& operator = (const CStreamBase& stream) { // There are risks. CUDPStream udppstream = static_cast< CUDPStream >(stream); ..... return *this; } }; // Examples of virtual assignments CTCPStream tcpstream; CUDPStream udpstream; udpstream = tcpstream; // Incorrect usage, but can be checked by the compiler
Consider the following example of virtual assignment: Container's inheritance lineage supports virtual assignment interfaces inherited from base class interfaces:
template <typename T> class Container { public: virtual Container & operator = (const T &) = 0; //... }; template <typename T> class List : public Container<T> { private: List & operator = (const T &); //... }; template <typename T> class Array : public Container<T> { private: Array & operator = (const T &); //... }; // Virtual Assignment Call Container<int> &c(getCurrentContainer()); c = 12;
If you look closely at the code above, you'll see that the operator = above is not what you would normally call an assignment operator, because the type of parameter is not a container type (why the return value of an assignment operator overridden by a derived class can be different from the base class type).
Here, the assignment operator is to set all elements in the Container to the same value. Such use of assignment operators can be misinterpreted.
A more secure interface does not use operator overloading, but instead uses nonoperator member functions that do not cause ambiguity:
template <typename T> class Container { public: virtual void setAll(const T &newElementValue) = 0; //... }; Container<int> &c(getCurrentContainer()); c.setAll(12);//Clear meaning
Copy assignment operators can also be declared virtual functions, but this is not the correct design, because even if a copy assignment operator is written in a derived class, it is not an override of a base class copy assignment operator.
template <typename T> class Container { public: virtual Container & operator = (const Container &) = 0; }; template <typename T> class List : public Container<T> { List & operator = (List &); //Not rewritten List & operator = (Container<T> &); //That's a rewrite }; //... Container<int> &c1 = getMeAList(); Container<int> &c2 = getMeAnArray(); c1 = c2; //Assign array to list
The virtual assignment operator makes it possible for one derived class object to assign values to another entirely different derived class object. This should have been prohibited.
A better solution is to use a non-virtual member function of type Container with the name copyContent, extract the value from the replication source using an iterator, and insert it for the replication purpose with the following code:
Container<T> &c1 = getMeAList(); Container<T> &c2 = getMeAnArray(); c1.copyContent(c2);//Copy the contents of array to list
This is what the Standard Library does, with the following code:
vector<int> v; list<int> e1(v.begin(), v.end());
Prototype Pattern is a better method than virtual assignment: the base class provides a pure virtual function named clone, then the derived class overrides it, returning a reference or pointer to a derived class object.
//container.h template <typename T> class Container { public: virtual Container *clone() const = 0; }; template <typename T> class List : public Container<T> { public: List (const List &); List *clone() const { return new List(*this); } }; template <typename T> class Array : public Container<T> { Array(const Array &); Array *clone() const { return new Array(*this); } }; Container<int> *cp = getCurrentContainer(); Container<int> *cp2 = cp->clone();
The prototype pattern, like "I don't know exactly what I'm pointing to, but I want to get one that's exactly the same."
Keep in mind
- Avoid virtual replication whenever possible, and instead implement a replicated copy of the object by prototyping it.