How to clone as derived object in C++
Asked Answered
E

4

11

I define two classes in C++. Ones is the base class, and one is a derived class

    class CBaseClass
    {
    …
    }

    class CDerivedClass : public CBaseClass
    {
    …
    }

And want to implement a clone function as follows:

    CBaseClass *Clone(const CBaseClass *pObject)
    {
    }

When an object of CDerivedClass is passed to Clone, then the function will also create a CDerivedClass object and return. When an object of CBaseClass is passed to Clone, then the function will also create a CBaseClass object and return.

How to implement such a feature?

Extragalactic answered 24/7, 2014 at 16:47 Comment(3)
How about a virtual member function clone?Trinitrocresol
Clone should be a virtual member method implemented by each class of your hierachy, including CDerivedClassBlesbok
possible duplicate of How to clone object in C++ ? Or Is there another solution?Blesbok
I
6

The virtual clone pattern is often used to solve problems such as these. Classic solutions tend to use co-variant return types for the clone() method. Other solution inject a factory type class (using CRTP) between the base and the derived classes. There are even solutions that just implement this functionality with macros. See the C++ FAQ, C++ idioms and a blog on this. Any one of these solutions is viable and the most appropriate would depend on the context in which they are used and intended to be used.

A classic approach, using covariant return types and coupled with more modern RAII techniques (shared_ptr et. al.) offers a very flexible and safe combination. One of the advantages of the covariant return type is that you are able to obtain a clone at the same level in the hierarchy as the argument (i.e. the return is not always to the base class).

The solution does require access to shared_ptr and/or unique_ptr. If not available with your compiler, boost provides alternatives for these. The clone_shared and clone_unique are modelled on the corresponding make_shared and make_unique utilities form the standard library. They contain explicit type checks on the class hierarchy of the arguments and target types.

#include <type_traits>
#include <utility>
#include <memory>

class CBaseClass {
public:
  virtual CBaseClass * clone() const {
    return new CBaseClass(*this);
  }
};

class CDerivedClass : public CBaseClass {
public:
  virtual CDerivedClass * clone() const {
    return new CDerivedClass(*this);
  }
};

class CMoreDerivedClass : public CDerivedClass {
public:
  virtual CMoreDerivedClass * clone() const {
    return new CMoreDerivedClass(*this);
  }
};

class CAnotherDerivedClass : public CBaseClass {
public:
  virtual CAnotherDerivedClass * clone() const {
    return new CAnotherDerivedClass(*this);
  }
};

// Clone factories

template <typename Class, typename T>
std::unique_ptr<Class> clone_unique(T&& source)
{
  static_assert(std::is_base_of<Class, typename std::decay<decltype(*source)>::type>::value,
    "can only clone for pointers to the target type (or base thereof)");
  return std::unique_ptr<Class>(source->clone());
}

template <typename Class, typename T>
std::shared_ptr<Class> clone_shared(T&& source)
{
  static_assert(std::is_base_of<Class, typename std::decay<decltype(*source)>::type>::value,
    "can only clone for pointers to the target type (or base thereof)");
  return std::shared_ptr<Class>(source->clone());
}

int main()
{
  std::unique_ptr<CDerivedClass> mdc(new CMoreDerivedClass()); // = std::make_unique<CMoreDerivedClass>();
  std::shared_ptr<CDerivedClass> cloned1 = clone_shared<CDerivedClass>(mdc);
  std::unique_ptr<CBaseClass> cloned2 = clone_unique<CBaseClass>(mdc);
  const std::unique_ptr<CBaseClass> cloned3 = clone_unique<CBaseClass>(mdc);
  // these all generate compiler errors
  //std::unique_ptr<CAnotherDerivedClass> cloned4 = clone_unique<CAnotherDerivedClass>(mdc);
  //std::unique_ptr<CDerivedClass> cloned5 = clone_unique<CBaseClass>(mdc);
  //auto cloned6 = clone_unique<CMoreDerivedClass>(mdc);
}

I've added a CMoreDerivedClass and CAnotherDerivedClass to expand the hierarchy a little to better show types checks etc.

Sample code

Illusionism answered 25/7, 2014 at 8:45 Comment(3)
You will want to call make_shared to avoid memory allocation overhead when cloning shared pointers.Nalley
@jxh, good point. make_shared though creates the object itself, which is what the clone method is also doing; hence a conflict because the clone_shared method doesn't actually know what the most derived type of its argument is (only the relationship between the argument of the target). I think it could still be possible though, but I felt it beyond the scope of the question. I think the clone_unique would almost always make more sense, but the code isn't limited to that.Illusionism
Knowledge of the derived class is the advantage CRTP provides. Neither approach offers a perfect solution, I concur.Nalley
M
2

Here is a simple solution. Remember to provide a Clone for each class in the inheritance.

class Base
{
public:
    virtual ~Base() {}
    virtual Base *Clone() const
    {
        // code to copy stuff here
        return new Base(*this);
    }
};

class Derived : public Base
{
public:
    virtual Derived *Clone() const
    {
        // code to copy stuff here
        return new Derived(*this);
    }
};
Monometallic answered 24/7, 2014 at 16:54 Comment(16)
Nice use of covariant return type, but that probably needs some additional explanation.Battista
@Puppy Covariant return didn't work with unique_ptr so I gave up. Please advise if you have a better solution.Monometallic
@NeilKirk: The only time you need to clone is when you don't know the most derived type anyway, so there's no downside to simply returning a std::unique_ptr<Base> that I can think of.Ancalin
What's the advantage of using covariant return here?Gitlow
@Puppy Why on earth would you use anything but a raw pointer when cloning? And why would you overload the works with CRTP? The first is probably a design error, and CRTP is just unnecessary additional complication.Gitlow
@MooingDuck The disadvantage is that the std::unique_ptr will delete the object if it accidentally goes out of scope, and you generally don't want that when cloning; it will lead to dangling pointers and a double delete down the road.Gitlow
I agree with Mooing Duck, they're should be no issue with unique_ptr<Base>, but if the covariant return is important, you can create a make style, akin to make_unique, factory that immediately binds it to the unique_ptr that is desired.Illusionism
@JamesKanze: If the variable accidentally goes out of scope, the advantage is that the std::unique_ptr will delete the object. Anything else would probably be a memory leak.Ancalin
@JamesKanze: The benefit of CRTP is that it offloads the burden of implementing boiler-plate clone code from every derived instance of the base, at the cost of derived classes have to remember to inherit from the CRTP helper.Nalley
@JamesKanze Because I may have car -> vehicle, and a function dealing with cars, and I want to use their clones as cars without casting. I don't know what brand of car it is.Monometallic
@MooingDuck The problem with std::unique_ptr is that it will delete the object, probably resulting in a dangling pointer. The usual reason for creating an object on the heap is that it has an arbitrary lifetime that doesn't respect scope. std::unique_ptr is useful in factory methods and such, where the object hasn't become fully live. But after a clone, I would expect that the object is fully live, and std::unique_ptr is more likely a source of problems than anything else.Gitlow
@Nalley So what is the large amount of boiler plate code that it replaces here. Using CRTP for clone will in fact increase the amount of boiler plate, requiring an additional derivation, etc. In this case, it's just extra complexity, for no benefit.Gitlow
@NeilKirk Circumstances vary, but most of the time, the context of clone means that you're dealing exclusively with base class pointers anyway.Gitlow
@JamesKanze: The "largeness" depends on how many derived types there are, and there is no "additional" derivation, just a different one.Nalley
@Nalley The additional source code complexity is O(n) for n derived classes. Both with the classical implementation and CRTP. The constant factor is slightly smaller with the classical implementation. As for what is behind the source code, CRTP is significantly more complicated.Gitlow
@JamesKanze: I am not sure where the smaller constant factor is coming from. The CRTP solution I present only requires to change the parent from the interface to the CRTP wrapper, instead of inheriting from base and implementing the cloning code. If using the CRTP wrapper, any fixes and additional cloning functionality would be inherited by all users of the wrapper, and not need individual implementation in each derived class.Nalley
F
1

With C++23's explicit object parameters, you can do this entirely within the base class.

class CBaseClass
{
    template <typename Self>
    std::unique_ptr<Self> Clone(this const Self& self) const {
        return std::make_unique<Self>(self);
    }
…
};

With this, you don't really need the free function clone, but it's simple

std::unique_ptr<CBaseClass> Clone(const CBaseClass* base) {
    return Base ? Base->Clone() : nullptr;
}
Ferreous answered 29/4 at 10:53 Comment(0)
N
0

You can accomplish this with a virtual Clone method, and a helper template CRTP class to implement this interface:

class CBaseClass {
    //...
    virtual CBaseClass * Clone () = 0;
    std::unique_ptr<CBaseClass> UniqueClone () {
        return std::unique_ptr<CBaseClass>(Clone());
    }
    virtual std::shared_ptr<CBaseClass> SharedClone () = 0;
};

template <typename DERIVED>
class CBaseClassCRTP : public CBaseClass
{
    CBaseClass * Clone () {
        return new DERIVED(*static_cast<DERIVED *>(this));
    }
    std::shared_ptr<CBaseClass> SharedClone () {
        return std::make_shared<CbaseClass>(*static_cast<DERIVED *>(this));
    }
};

class CDerivedClass : public CBaseClassCRTP<CDerivedClass>
{
    //...
};

Now, each derived class gete a Clone method courtesy of the helper class.

Nalley answered 24/7, 2014 at 16:53 Comment(4)
What happens if you have a Base pointer and don't know the exact type? You should return DERIVED *Monometallic
dynamic_cast is unnecessary and you should use a smart pointer here.Subvention
It seems odd having to pass the object-to-be-cloned into the cloning function. I'm accustomed to invoking cloning like this: cloned_object = some_object.clone().Titan
It seems very odd that something you'd clone will be managed by a std::unique_ptr; it's presumably complete after the cloning, and doesn't need any further management.Gitlow

© 2022 - 2024 — McMap. All rights reserved.