Polymorphic clones in modern C++
How to copy an object that is accessible only by an interface that it implements?
This question has been around for a very long time, and is associated with a classical solution described by Scott Meyers in Item 25 of More Effective C++. This solution still works, but can benefit from modern C++ features that weren’t in the standard when the book came out, in particular smart pointers.
I’m going to go over a quick reminder of the problem and the classical solution, and then show how throwing a bunch of smart pointers in the party can make the code more robust and more expressive, if we agree on certain conventions.
This post is part of the series Smart Developers Use Smart Pointers:
- Smart pointer basics
- unique_ptr, shared_ptr, weak_ptr, scoped_ptr, raw pointers: clearly stating your intentions by knowing your smart pointers
- Custom deleters and How to make them more expressive
- Changing deleters during the life of a unique_ptr
- How to implement the pimpl idiom by using unique_ptr
- How to make a polymorphic clone in modern C++
- How to Return a Smart Pointer AND Use Covariance (by Raoul Borges)
The classical problem
Let’s take the example of the following interface:
class Interface { public: virtual void doSomething() const = 0; virtual ~Interface() = default; };
With one the classes implementing this interface:
class Implementation : public Interface { public: virtual void doSomething() const override { /* ... */ } };
How to make a copy of the Implementation
object?
If you have access to the object itself, there is nothing easier:
Implementation x = // ... Implementation y = x;
But the existence of the Interface
suggests that there are polymorphic contexts where the object is accessible only via the interface:
Interface& x = // ... Interface& y = ??
And there is a problem here because, in C++, to construct an object we must spell out in the code the actual type of the object to be constructed (except in the case of implicit conversions). And here we don’t know what this type is. It could be Implementation
, or any other class inheriting from Interface
.
And even if, for some reason, we knew for sure that it was an Implementation
, the calling code may not have access to this class, which is one of the purposes of having an interface in the first place.
What to do then?
The classical solution
The classical solution is to “virtualize” the constructor, as Scott Meyers puts it. That is to say add a clone
method in the interface, that delegates the object construction to the implementation itself. The interface then looks like:
class Interface { public: virtual Interface* clone() const = 0; virtual void doSomething() const = 0; virtual ~Interface() = default; };
and on the implementation’s side:
class Implementation : public Interface { public: virtual Implementation* clone() const override { return new Implementation(*this); } virtual void doSomething() const override { /* ... */ } };
(override
wasn’t in the original solution, since it appeared in C++11, but it’s still a good practice to use it).
So the copy of the interface at call site looks like this:
Interface& x = // ... Interface* y = x.clone();
Notice that the return type of the clone
method differ between the interface in the implementation. It is because C++ allows overriding a virtual method with one that has a different return type, provided this return type is a pointer (resp. reference) to a class convertible to the one pointed to (resp. referenced by) the return type of the base class. This is called covariance.
This technique allows the desired copy, but exhibits another classical problem: the call site receives the responsibility to delete the cloned object, but nothing ensures that it will do it. Particularly if there is an early return or an exception thrown further down the code, the object has a risk to leak.
A modern solution
The tool cut out for solving this problem are smart pointers, and in particular std::unique_ptr
.
The idea is to make the clone function return a unique_ptr, that will take care about deleting the new object in all situations. Here is how to adapt the code with this:
class Interface { public: virtual std::unique_ptr<Interface> clone() const = 0; virtual void doSomething() const = 0; virtual ~Interface() = default; }; class Implementation : public Interface { public: virtual std::unique_ptr<Interface> clone() const override { return std::make_unique<Implementation>(*this); } virtual void doSomething() const override { /* ... */ } };
And at call site:
Interface& x = // ... std::unique_ptr<Interface> y = x.clone();
Let’s look at this solution more closely.
First, your compiler may not have std::make_unique
since it arrived in C++14 while std::unique_ptr
only came in C++11 (I believe this was just an oversight). If so, you can use this implementation proposed by cppreference.com:
// note: this implementation does not disable this overload for array types template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); }
Second, and much more annoyingly, the covariance doesn’t hold any more, because the clone
method is no longer returning pointers. It now has to return an std::unique_ptr<Interface>
in the interface AND in the implementation.
In the above case it doesn’t cause any practical problem, given that Implementation
already depends on Interface
anyway. But let’s consider the case where an implementation inherits from several interfaces. The solution without smart pointers scales effortlessly because the clone
method is independent from the interface:
class Interface1 { public: virtual Interface1* clone() const = 0; virtual void doSomething() const = 0; virtual ~Interface1() = default; }; class Interface2 { public: virtual Interface2* clone() const = 0; virtual void doSomethingElse() const = 0; virtual ~Interface2() = default; }; class Implementation : public Interface1, public Interface2 { public: virtual Implementation* clone() const override { return new Implementation(*this); } virtual void doSomething() const override { /* ... */ } virtual void doSomethingElse() const override { /* ... */ } };
But with smart pointers, the situation is different: the clone
method, bound to Interface1
, cannot be used for Interface2
! And since the clone
method doesn’t take any argument, there is no way to add a new overload returning a unique_ptr to Interface2
.
One solution that comes to mind is to use template methods. But there is no such such thing as a template virtual method so this solution is off the table.
Another idea would be to isolate the clone
method in a clonable
interface. But this would force the call site to dynamic_cast
back and forth from the real interface to the clonable interface. Not good either.
Clearing the ambiguity
The alternative I would suggest is to use different names for the clone methods in the interfaces.
The code would then look like:
class Interface1 { public: virtual std::unique_ptr<Interface1> cloneInterface1() const = 0; virtual void doSomething() const = 0; virtual ~Interface1() = default; }; class Interface2 { public: virtual std::unique_ptr<Interface2> cloneInterface2() const = 0; virtual void doSomethingElse() const = 0; virtual ~Interface2() = default; }; class Implementation : public Interface1, public Interface2 { public: virtual std::unique_ptr<Interface1> cloneInterface1() const override { return make_unique<Implementation>(*this); } virtual std::unique_ptr<Interface2> cloneInterface2() const override { return make_unique<Implementation>(*this); } virtual void doSomething() const override { } virtual void doSomethingElse() const override { } };
But to be viable, this solution has to rely on a guideline for interface designers: if you choose to implement a clone method that returns a smart pointer, then don’t call it just clone
.
Rather, use a specific name, like cloneInterfaceX
, that won’t conflict with the copy functions coming from the other interfaces.
This way, you allow implementers to use your interface even if they already use others.
As Aristotle would have it, Man is a social animal. Let us developers take example and let our interfaces live together without getting in conflict with each other, and die with dignity, that is, by being sure to be called on their destructors.
Now this is a solution for this particular problem, but there is a bigger C++ question behind this: how to make smart pointers work with covariance? You will have the answer on the next post, written by Raoul Borges who’s much more experienced than me on that question.
Related articles:
- Smart pointer basics
- unique_ptr, shared_ptr, weak_ptr, scoped_ptr, raw pointers: clearly stating your intentions by knowing your smart pointers
- Custom deleters and How to make them more expressive
- Changing deleters during the life of a unique_ptr
- How to implement the pimpl idiom by using unique_ptr
- How to Return a Smart Pointer AND Use Covariance (by Raoul Borges)
Share this post!