Jonathan Boccara's blog

How to Assign Derived Classes in C++

Published May 22, 2020 - 0 Comments

A classical problem in object-oriented programming, and with polymorphism in general, is to handle multiple polymorphic objects at the same time. In other terms, multiple dispatch.

An associated problem with object-oriented programming is that many languages, including C++, don’t implement multiple dispatch.

One case comes up often: handling the behaviour of two objects of the same derived class.

One case in this special case comes up often: assigning an object to another one.

Let’s consider the example of a base class X:

class X
{
public:
    // interface of X...
    virtual ~X() = 0;
};

With two derived classes A and B:

class A : public X
{
    // ...
};

class B : public X
{
    // ...
};

If we have two concrete objects that we manipulate through their base class as references to X, how can we implement an assignment operator to assign one into the other?

For example, with x1 and x2 being references to X, how do we assign one into the other? The problem is that the following is not correct:

x1 = x2;

This statement calls the operator= of class X, which is not virtual. It assigns the members of class X if it has any, but it won’t assign the members of the derived classes.

What’s a C++ programmer to do?

We’re going to see several solutions. I’ll tell you right away, none of them is ideal, they have advantages and drawbacks. They work around the fact that C++ doesn’t have multiple dispatch. If you have a better solution, I’d love to read about it, please leave a comment.

Implementing the assignment in each class

One solution is to make operator= virtual and implement it in each derived class.

In the interface X we then declare:

class X
{
public:
    virtual X& operator=(X const& other) = 0;
    virtual ~X() = 0;
};

We need to provide an implementation in X for this virtual operator= as the operator= in derived classes call their base classes’, and the fact that we declare it virtual prevents the compiler to generate it for us.

Unless X has complicated data members, we can write this:

X& X::operator=(X const& other) = default;

Then in the base classes, we implement this virtual operator=. Note that this is not the default operator= for the derived class, because the virtual operator= takes a base object as parameter whereas the default operator= of the derived class takes a derived object as parameter.

For class A:

class A : public X
{
public:
    A& operator=(X const& other) override
    {
        if (auto* aOther = dynamic_cast<A const*>(&other))
        {
            *this = *aOther;
        }
        return *this;
    }
    // ...
};

For class B:

class B : public X
{
public:
    B& operator=(X const& other) override
    {
        if (auto* bOther = dynamic_cast<B const*>(&other))
        {
            *this = *bOther;
        }
        return *this;
    }
    // ...
};

This code checks that the object to assign from is indeed of the same type as the one to assign to, and then calls the default assignment operator of the derived class. Otherwise it does nothing.

We could also introduce error handling, to do something in the case that tries to assign a derived class to another one:

class A : public X
{
public:
    A& operator=(X const& other) override
    {
        if (auto* aOther = dynamic_cast<A const*>(&other))
        {
            *this = *aOther;
        }
        else
        {
            // error handling code here
        }
        return *this;
    }
    // ...
};

class B : public X
{
public:
    B& operator=(X const& other) override
    {
        if (auto* bOther = dynamic_cast<B const*>(&other))
        {
            *this = *bOther;
        }
        else
        {
            // error handling code here
        }
        return *this;
    }
    // ...
};

Here is a complete code example to illustrate this technique.

This solution is simple but has the drawback introducing ugly code with dynamic_cast and error handling, and what’s more it duplicates it throughout the whole hierarchy.

Let’s see another solution that packages this code into one place.

A CRTP class

One place we can offload this code is into a CRTP base class.

The CRTP is a pattern where a base class knows the type of its derived class. We can add such a base class that takes care of the dynamic_cast.

This base class could look like this (we’ll improve it later on — except its name, if you see a better name please let me know!):

template<typename Derived, typename Base>
struct VirtualAssignable
{
    Derived& assignFrom(Base const& other)
    {
        auto& thisDerived = static_cast<Derived&>(*this);
        if (auto* otherDerived = dynamic_cast<Derived const*>(&other))
        {
            thisDerived = *otherDerived;
        }
        else
        {
            // error handling
        }
        return thisDerived;
    }
};

If the type of the object to assign from is the derived class, then this helper downcasts itself into our derived class (it can as our derived class will inherit from it), and performs the assignment.

Our derived classes then looks like this:

class A : public X, public VirtualAssignable<A, X>
{
public:
    A& operator=(X const& other) override
    {
        return assignFrom(other);
    }
    // ...
};

class B : public X, public VirtualAssignable<B, X>
{
public:
    B& operator=(X const& other) override
    {
        return assignFrom(other);
    }
    // ...
};

Here is a complete code example to illustrate this technique.

There is now less boilerplate code in the implementation of the derived classes, but we can go further: this remaining code makes the connection between the base class and VirtualAssignable. Why would the derived class be in charge of making this connection? It would be easier to connect VirtualAssignable with the base class itself!

The CRTP as an intermediary

To do this, we remove the direct inheritance relationship between A and X, and make VirtualAssignable the only base class of our derived classes.

VirtualAssignable can then take care of implementing the virtual operator=:

template<typename Derived, typename Base>
struct VirtualAssignable : Base
{
    VirtualAssignable& operator=(Base const& other) override
    {
        auto& thisDerived = static_cast<Derived&>(*this);
        if (auto* otherDerived = dynamic_cast<Derived const*>(&other))
        {
            thisDerived = *otherDerived;
        }
        else
        {
            // error handling
        }
        return thisDerived;
    }
};

The code of the derived classes then becomes:

class A : public VirtualAssignable<A, X>
{
    // ...
};

class B : public VirtualAssignable<B, X>
{
    // ...
};

Here is a complete code example illustrating this technique.

This is better than the previous solution as VirtualAssignable takes care of everything. It is emulating an automatic generation of operator=, with polymorphism.

The advantage over the first solution that used a dynamic_cast in each derived class is that now, the derived classes are rid of the boilerplate.

The drawback over that first solution is that the inheritance line looks weird: class A : public X is clearer than class A : public VirtualAssignable<A, X>. If you’re familiar with VirtualAssignable then it’s fine, otherwise it makes this line difficult to read.

Adding useful features

There are three useful features we can add to VirtualAssignable: a CRTP protection, a base class alias, and a customizable error handling.

The first one is a classical trick related to the CRTP, to prevent passing the wrong derived class. For example:

class B : public VirtualAssignable<A, X> // oops!

This can happen with a hasty copy-paste. To prevent that code from compiling, we can make the constructor of the CRTP base class private, and make the CRTP base class friend with the derived class, so that only it can call it:

template<typename Derived, typename Base>
class VirtualAssignable : Base
{
public:
    VirtualAssignable& operator=(Base const& other) override
    {
        auto& thisDerived = static_cast<Derived&>(*this);
        if (auto* otherDerived = dynamic_cast<Derived const*>(&other))
        {
            thisDerived = *otherDerived;
        }
        else
        {
            // error handling
        }
        return thisDerived;
    }

private:
    VirtualAssignable(){}
    friend Derived;
};

The second feature to add is a helper to access this class from the derived class. VirtualAssignable<A, X> is a mouthful, and even more so if you place it in a namespace. For the implementation code that needs the type of the base class, we can provide an alias to emulate the “super” keyword that Java has.

In general, we would place this alias in the protected section, but as Abel is pointing out in the comments section, since the derived class is a friend we can place it in the private section:

template<typename Derived, typename Base>
class VirtualAssignable : Base
{
public:
    VirtualAssignable& operator=(Base const& other) override
    {
        auto& thisDerived = static_cast<Derived&>(*this);
        if (auto* otherDerived = dynamic_cast<Derived const*>(&other))
        {
            thisDerived = *otherDerived;
        }
        else
        {
            // error handling
        }
        return thisDerived;
    }

private:
    VirtualAssignable(){}
    friend Derived;
    using base = VirtualAssignable; 
};

Then for example if the derived class has a custom implementation for its copy constructor, it has to copy it base classes too:

A::A(A const& other)
: base(other),
  // copying other members...
{
}

In theory, classes should rarely need to write their own copy constructor, because it means they do some custom handling of resources. But in practice, the practice is not like the theory, especially with legacy code.

The third feature we can add is the possibility to customize the error handling:

template<typename Derived, typename Base, typename ErrorHandlingFunction = AssertCompatibleTypeFailed>
class VirtualAssignable : Base
{
public:
    VirtualAssignable& operator=(Base const& other) override
    {
        auto& thisDerived = static_cast<Derived&>(*this);
        if (auto* otherDerived = dynamic_cast<Derived const*>(&other))
        {
            thisDerived = *otherDerived;
        }
        else
        {
            ErrorHandlingFunction{}();
        }
        return thisDerived;
    }

private:
    VirtualAssignable(){}
    friend Derived;
    using base = VirtualAssignable;
};

This allows a user of VirtualAssignable to specify how to react in case we’re trying to assign a concrete type into another one. We provide a default so that we don’t force user to specify the parameter.

One possible default is to assert that the execution is not going into this code, for example with this type:

struct AssertCompatibleTypeFailed
{
   void operator()();
};

Its implementation (that can be in a separate .cpp file) can look like this:

void AssertCompatibleTypeFailed::operator()()
{
    assert(("Incompatible types for assignment", false));
}

A piece of code using VirtualAssignable can then provide its own function type to have a different way of handling errors.

A polymorphic operator=

With VirtualAssignable we emulated the automatic generation of a polymorphic operator=.

Do you ever need to assign polymorphic classes?

How would you like to see VirtualAssignable improved?

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin