How to make a better polymorphic clone in modern C++

This short port is an answer to the blog post of @JoBoccara on how to make a polymorphic clone in modern C++ in the presence of smart pointers. The goal of this post is to explain in which aspects the solution of the OP is not really satisfying, and to provide what I believe to be better alternatives.

 

Quick summary of the problem


If you have not read the original post, here is quick summary of the C++ challenge to solve. The goal is to implement a polymorphic clone for classes implementing at least two interfaces.

 

The classic solution

The classic solution relies on the so called “covariance” available in C++ (it is not really covariance). We profit from the fact that the clone of the Implementation class can return a pointer to Implementation.

Here is how it looks for a single interface:

struct Interface
{
virtual Interface* clone() const = 0;
//...
};
class Implementation : public Interface
{
public:
virtual Implementation* clone() const override
{
return new Implementation(*this);
}
//...
};

Are is he how it looks for multiple interfaces (we can note that thanks to this trick, we only need to define one clone implementation in the child class):

struct Interface1
{
virtual Interface1* clone() const = 0;
//...
};
struct Interface2
{
virtual Interface2* clone() const = 0;
//...
};
class Implementation : public Interface1, public Interface2
{
public:
virtual Implementation* clone() const override
{
return new Implementation(*this);
}
//...
};

Now, the OP mentions that this solution is not satisfying enough, because of the unsafe resource management. Returning a pointer does not declare the intent and whose responsible for releasing this resource.

 

The solution of the OP

The problem is that what we call “covariance” in C++ does not do the trick anymore if we try to introduce smart pointers. We do not have the ability to return a smart pointer to Implementation in the child class as it only works for pointers. The solution proposed by the OP is to name the clone differently for the different interfaces.

Each clone method is renamed to have the name of its interface appearing in it:

struct Interface1
{
virtual std::unique_ptr<Interface1> cloneInterface1() const = 0;
//...
};
struct Interface2
{
virtual std::unique_ptr<Interface2> cloneInterface2() const = 0;
//...
};
class Implementation : public Interface1, public Interface2
{
public:
virtual std::unique_ptr<Interface1> cloneInterface1() const override { /* ... */ }
virtual std::unique_ptr<Interface2> cloneInterface2() const override { /* ... */ }
//...
};

Notice that we now need two implementations of clone instead of just one.

 

The drawbacks of the OP solution


Introducing N different names for the same concept appearing in N different interfaces has some significant drawbacks, among which:

 
It adds unnecessary specificity both for our brain and the compiler. In particular, we loose the ability to use the kind of duck typing that templates offer us in C++. From a design perspective, we would like our interface to implement the same meta-interface (it would be a C++20 concept on the interfaces).

It adds redundancy: the child class has to implement a new clone method for each of the interfaces it implements. This is noise that we would like to avoid. This redundancy is also annoying on the client side, where the type is repeated twice.

It is pretty fragile too: everyone must be careful about naming its clone method such that the name of the interface appears in it. This couples the name of the method with the interface name. And it is likely to break upon, for instance, renaming the interface.

It is incomplete and limited. This solution does not take into account namespaces. We can still have two interfaces with the same the name, in two different namespaces, and this trick will not work (unless we also add the name of the namespace in the name of the clone method).

And finally, it is intrusive. It requires modifying all interfaces to follow this pattern, which is very unlikely to happen if your code deals with already existing code, or code outside of your control.

 

Gradually improving on the solution


In this section, we will discuss on how we can gradually improve on the solution, and propose several alternatives. We will also discuss about trade-offs and risk management.

 

Rolling-back

A first improvement would be to roll-back to the classic solution with naked pointer and forget entirely about integrating smart pointers in the clone method:

struct Interface1
{
virtual Interface1* clone() const = 0;
//...
};
struct Interface2
{
virtual Interface2* clone() const = 0;
//...
};
class Implementation : public Interface1, public Interface2
{
public:
virtual Implementation* clone() const override
{
return new Implementation(*this);
}
//...
};

I would argue that it would already make for a better solution than the one proposed by the OP.

Granted, it is not 100% safe in terms of resource management, but we have to consider the downsides (see previous section) of integrating the smart pointers in the clone method.

It is all about cost versus benefit. Is the reduction in debugging time worth the added complexity? What would be the impact of the added complexity on development time? Can tools monitor help us find the resource leaks instead? Is cloning an interface such a common (and advisable) use case?

 

Improving on resource safety (non-intrusive solution)

Now, to improve the safety of the classic solution, without having to intrude into the definition of the interfaces (they might not be under our control), we can use a classic trick of adding a new level of indirection.

Instead of trying to mix the concern of memory management inside the clone method, we can separate concerns and provide a free function named clone which will help with memory management:

template<typename Clonable>
std::unique_ptr<Clonable> clone(Clonable const* i)
{
return std::unique_ptr<Clonable>(i->clone());
}

Now, the client code only needs to follow a convention which has much less drawbacks than the one proposed by the OP: calling the clone free function instead of calling the method directly.

Implementation impl;
Interface1* i1 = &impl;
std::unique_ptr<Interface1> c1 = clone(i1);
Interface2* i2 = &impl;
std::unique_ptr<Interface2> c2 = clone(i2);

To send a stronger message to the user, we could rename the clone method into cloneImpl. It would discourage direct usage of the method, and instead encourage the use of the clone free function.

 

Improving on resource safety (intrusive solution)

The previous solution tried to limit the risks of resource leaks, while not requiring any modification to existing interfaces. We will now look at another solution, which we could consider safer in terms of resource management, but which requires explicit changes in the interfaces.

We will rely on Non Virtual Interface idiom and the Curiously Recurring Template Pattern. Again, the key point of this solution is to separate concerns between the cloning and the resource management.

NVI allows to separate the memory management from the clone implementation details:

  • The clone method will only be responsible for the wrapping with a unique_ptr
  • The cloneImpl virtual method allows child classes to implement the cloning itself
class Interface
{
public:
//...
std::unique_ptr<Interface> clone()
{
return std::unique_ptr<Interface>(cloneImpl());
}
protected:
virtual Interface* cloneImpl() const = 0;
};
view raw NVI.hpp hosted with ❤ by GitHub

We then use CRTP to factorize this common behavior to all clonable interfaces:

template<typename Derived>
struct Clonable
{
virtual ~Clonable() = default;
std::unique_ptr<Derived> clone()
{
return std::unique_ptr<Derived>(cloneImpl());
}
protected:
virtual Derived* cloneImpl() const = 0;
};
view raw Clonable.hpp hosted with ❤ by GitHub

And to automate the deployment of this policy in our interfaces (whose clone method will now come from the policy):

struct Interface1 : Clonable<Interface1> { /* ... */ };
struct Interface2 : Clonable<Interface2> { /* ... */ };
class Implementation : public Interface1, public Interface2
{
//...
protected:
virtual Implementation* cloneImpl() const override
{
return new Implementation(*this);
}
};

We now have access to a polymorphic clone method which returns a smart pointer and works for classes which implement several interfaces:

Implementation impl;
Interface1* i1 = &impl;
std::unique_ptr<Interface1> c1 = i1->clone();
Interface2* i2 = &impl;
std::unique_ptr<Interface2> c2 = i2->clone();

Please note that this solution comes with some drawbacks though. In particular, trying to call clone on the implementation directly is ambiguous. It will be rejected by the compiler.

 

Improving on resource safety (with tools)

Finally, we also have to think “tools”. We could further improve both safety and expressiveness of our non-intrusive solution, by relying on the GSL and its owner pointer.

It is quite a useful alias to use when smart pointers do not fit the bill:

struct Interface1
{
virtual owner<Interface1*> clone() const = 0;
//...
};
struct Interface2
{
virtual owner<Interface2*> clone() const = 0;
//...
};
class Implementation : public Interface1, public Interface2
{
public:
virtual owner<Implementation*> clone() const override
{
return new Implementation(*this);
}
//...
};

Combined with our non-intrusive solution, it would ensure that the client code either calls the clone free function (in which case it is safe) or deals with resource management correctly with static analysis.

 

Conclusion & takeaway


When we push safety to its limits, we risk falling into paranoia. There are risks that are not worth eliminating considering the costs involved in doing so. There are always trade-offs to take into account. Not considering the drawbacks might lead to increase complexity unnecessarily.

Instead of trying to eliminate all risks completely, we can try to reduce them at an acceptable level, with an acceptable cost. We have to realize that eliminating risk completely is not even possible: even with smart pointers, there is a risk of someone calling release on it, and letting the resource leak.

When trying to build a solution to reduce the risk, we can think of resource management or polymorphism as separate concerns. It might help us reason differently and find more decoupled solutions.

Finally, we do not have to put everything into code. We can rely on external tooling to get rid of the remaining risks. It may, in some case, be more effective and less expensive. Training and learning can help too: most C++ developers know about smart pointers and will likely wrap the returned clone with a smart pointer (and one of their choice).


Comments are closed.

Create a website or blog at WordPress.com

Up ↑