c++object-slicingcloneable

How to implement ICloneable without inviting future object-slicing


My question is about how to implement the classic ICloneable interface in such a way that it won't lead to inadvertent object-slicing when a future programmer isn't paying close attention. Here's an example of the kind of programming error I'd like to detect (preferably at compile-time):

#include <stdio.h>

class ICloneable
{
public:
   virtual ICloneable * clone() const = 0;
};

class A : public ICloneable
{
public:
   A() {}
   A(const A & rhs) {}

   virtual ICloneable * clone() const {return new A(*this);}
};

class B : public A
{
public:
   B() {}
   B(const B & rhs) {}

   // Problem, B's programmer forget to add a clone() method here!
};

int main(int, char**)
{
   B b;
   ICloneable * clone = b.clone();  // d'oh!  (clone) points to an A, not a B!
   return 0;
}

Is there any way in C++ to convince the compiler to emit an error if B (or any further non-abstract subclasses of B) doesn't define its own clone() method? Short of that, is there any automatic way to detect this error at run-time?


Solution

  • It's a while ago that I faced the very same issue in the very same situation without finding a satisfying solution.

    Thinking about this again, I found something which might be a solution (at best):

    #include <iostream>
    #include <typeinfo>
    #include <typeindex>
    
    class Base { // abstract
      protected:
        Base() = default;
        Base(const Base&) = default;
        Base& operator=(const Base&) = default;
      public:
        virtual ~Base() = default;
        
        Base* clone() const
        {
          Base *pClone = this->onClone();
          const std::type_info &tiClone = typeid(*pClone);
          const std::type_info &tiThis = typeid(*this);
    #if 0 // in production
          assert(std::type_index(tiClone) == type_index(tiThis)
            && "Missing overload of onClone()!");
    #else // for demo
          if (std::type_index(tiClone) != std::type_index(tiThis)) {
            std::cout << "ERROR: Missing overload of onClone()!\n"
              << "  in " << tiThis.name() << '\n';
          }
    #endif // 0
          return pClone;
        }
        
      protected:
        virtual Base* onClone() const = 0;
    };
    
    class Instanceable: public Base {
      public:
        Instanceable() = default;
        Instanceable(const Instanceable&) = default;
        Instanceable& operator=(const Instanceable&) = default;
        virtual ~Instanceable() = default;
        
      protected:
        virtual Base* onClone() const { return new Instanceable(*this); }
    };
    
    class Derived: public Instanceable {
      public:
        Derived() = default;
        Derived(const Derived&) = default;
        Derived& operator=(const Derived&) = default;
        virtual ~Derived() = default;
        
      protected:
        virtual Base* onClone() const override { return new Derived(*this); }
    };
    
    class WrongDerived: public Derived {
      public:
        WrongDerived() = default;
        WrongDerived(const WrongDerived&) = default;
        WrongDerived& operator=(const WrongDerived&) = default;
        virtual ~WrongDerived() = default;
    
      // override missing
    };
    
    class BadDerived: public Derived {
      public:
        BadDerived() = default;
        BadDerived(const BadDerived&) = default;
        BadDerived& operator=(const BadDerived&) = default;
        virtual ~BadDerived() = default;
    
      // copy/paste error
      protected:
        virtual Base* onClone() const override { return new Derived(*this); }
    };
    
    #define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 
    
    int main()
    {
      DEBUG(Instanceable obj1);
      DEBUG(Base *pObj1Clone = obj1.clone());
      DEBUG(std::cout << "-> " << typeid(*pObj1Clone).name() << "\n\n");
      DEBUG(Derived obj2);
      DEBUG(Base *pObj2Clone = obj2.clone());
      DEBUG(std::cout << "-> " << typeid(*pObj2Clone).name() << "\n\n");
      DEBUG(WrongDerived obj3);
      DEBUG(Base *pObj3Clone = obj3.clone());
      DEBUG(std::cout << "-> " << typeid(*pObj3Clone).name() << "\n\n");
      DEBUG(BadDerived obj4);
      DEBUG(Base *pObj4Clone = obj4.clone());
      DEBUG(std::cout << "-> " << typeid(*pObj4Clone).name() << "\n\n");
    }
    

    Output:

    Instanceable obj1;
    Base *pObj1Clone = obj1.clone();
    std::cout << "-> " << typeid(*pObj1Clone).name() << '\n';
    -> 12Instanceable
    
    Derived obj2;
    Base *pObj2Clone = obj2.clone();
    std::cout << "-> " << typeid(*pObj2Clone).name() << '\n';
    -> 7Derived
    
    WrongDerived obj3;
    Base *pObj3Clone = obj3.clone();
    ERROR: Missing overload of onClone()!
      in 12WrongDerived
    std::cout << "-> " << typeid(*pObj3Clone).name() << '\n';
    -> 7Derived
    
    BadDerived obj4;
    Base *pObj4Clone = obj4.clone();
    ERROR: Missing overload of onClone()!
      in 10BadDerived
    std::cout << "-> " << typeid(*pObj4Clone).name() << '\n';
    -> 7Derived
    
    

    Live Demo on coliru

    The trick is actually quite easy:

    Instead of overriding clone() itself, it is used as trampoline into a virtual onClone() method. Hence, clone() can check the result for correctness before returning it.

    This is not a compile-time check but a run-time check (what I consider as second best option). Assuming that every class of a class library in development should hopefully be checked / debugged at least during development I find this quite reliable.


    The accepted answer to SO: Reusable member function in C++ showed me a way to make this even more immune against copy/paste errors:

    Instead of typing out the class name in every overridden clone(), the class name is obtained via decltype():

    class Instanceable: public Base {
      public:
        Instanceable() = default;
        Instanceable(const Instanceable&) = default;
        Instanceable& operator=(const Instanceable&) = default;
        virtual ~Instanceable() = default;
        
      protected:
        virtual Base* onClone() const
        {
          return new std::remove_const_t<std::remove_pointer_t<decltype(this)>>(*this);
        }
    };
    
    class Derived: public Instanceable {
      public:
        Derived() = default;
        Derived(const Derived&) = default;
        Derived& operator=(const Derived&) = default;
        virtual ~Derived() = default;
        
      protected:
        virtual Base* onClone() const override
        {
          return new std::remove_const_t<std::remove_pointer_t<decltype(this)>>(*this);
        }
    };
    

    Live Demo on coliru