Practical Experience 89 About Virtual Assignment

Keywords: Java C++

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.

Posted by emceej on Tue, 19 Oct 2021 10:39:15 -0700