Search code examples
c++smart-pointerscopy-constructorassignment-operator

How to approach copying objects with smart pointers as class attributes?


From the boost library documentation I read this:

Conceptually, smart pointers are seen as owning the object pointed to, and thus responsible for deletion of the object when it is no longer needed.

I have a very simple problem: I want to use RAII for pointer attributes of a class that is Copyable and Assignable.

The copy and assignment operations should be deep: every object should have its own copy of the actual data. Also, RTTI needs to be available for the attributes (their type may also be determined at runtime).

Should I be searching for an implementation of a Copyable smart pointer (the data are small, so I don't need Copy on Write pointers), or do I delegate the copy operation to the copy constructors of my objects as shown in this answer?

Which smart pointer do I choose for simple RAII of a class that is copyable and assignable? (I'm thinking that the unique_ptr with delegated copy/assignment operations to the class copy constructor and assignment operator would make a proper choice, but I am not sure)

Here's a pseudocode for the problem using raw pointers, it's just a problem description, not a running C++ code:

// Operation interface
class ModelOperation
{
    public: 
        virtual void operate = (); 
};

// Implementation of an operation called Special 
class SpecialModelOperation
:
    public ModelOperation
{
    private:
        // Private attributes are present here in a real implementation. 

    public: 

        // Implement operation
        void operate () {}; 
};

// All operations conform to ModelOperation interface
// These are possible operation names: 
// class MoreSpecialOperation; 
// class DifferentOperation; 

// Concrete model with different operations
class MyModel 
{
    private: 
        ModelOperation* firstOperation_; 
        ModelOperation* secondOperation_;  

    public:

        MyModel()
            : 
                firstOperation_(0), 
                secondOperation_(0)
        {
            // Forgetting about run-time type definition from input files here.
            firstOperation_  = new MoreSpecialOperation(); 
            secondOperation_ = new DifferentOperation(); 
        }

        void operate()
        {
            firstOperation_->operate(); 
            secondOperation_->operate();
        }

        ~MyModel() 
        {
            delete firstOperation_; 
            firstOperation_ = 0; 

            delete secondOperation_; 
            secondOperation_ = 0; 
        }
};

int main()
{

    MyModel modelOne; 

    // Some internal scope
    {
        // I want modelTwo to have its own set of copied, not referenced 
        // operations, and at the same time I need RAII to for the operations, 
        // deleting them automatically as soon as it goes out of scope. 
        // This saves me from writing destructors for different concrete models.  
        MyModel modelTwo (modelOne); 
    }


    return 0;
}

Solution

  • If you accept some requirements on your types, this can be done without requiring implementing virtual clone functions for all types. The particular requirements are that the types have accessible copy constructors, which some would deem undesirable because of potential for accidental slicing. Proper use of friending may mitigate the drawbacks of that, though.

    If such is acceptable one can go about this by erasing the derived types under an interface that provides copy functionality:

    template <typename Base>
    struct clonable {
        // virtual copy
        // this clone function will be generated via templates
        // no boilerplate is involved
        virtual std::unique_ptr<clonable<Base>> clone() const = 0;
    
        // expose the actual data
        virtual Base* get() = 0;
        virtual Base const* get() const = 0;
    
        // virtual destructor
        // note that this also obviates the need for a virtual destructor on Base
        // I would probably still make it virtual, though, just in case
        virtual ~clonable() = default;
    };
    

    This interface is implemented by a class templated on the most derived type, and thus knows how to make normal copies through the copy constructor.

    template <typename Base, typename Derived>
    struct clonable_holder : clonable<Base> {
        // I suppose other constructors could be provided
        // like a forwarding one for emplacing, but I am going for minimal here
        clonable_holder(Derived value)
        : storage(std::move(value)) {}
    
        // here we know the most derived type, so we can use the copy constructor
        // without risk of slicing
        std::unique_ptr<clonable<Base>> clone() const override {
            return std::unique_ptr<clonable<Base>>(new clonable_holder(storage));
        }
    
        Base* get() override { return &storage; }
        Base const* get() const override { return &storage; }
    
    private:
        Derived storage;
    };
    

    This will generate virtual copy functions for us without extra boilerplate. Now we can build a smart pointer-like class on top of this (not quite a smart pointer because it does not give pointer semantics, but value semantics instead).

    template <typename Base>
    struct polymorphic_value {
        // this constructor captures the most derived type and erases it
        // this is a point where slicing may still occur
        // so making it explicit may be desirable
        // we could force constructions through a forwarding factory class for extra safety
        template <typename Derived>
        polymorphic_value(Derived value)
        : handle(new clonable_holder<Base, Derived>(std::move(value))) {
            static_assert(std::is_base_of<Base, Derived>::value,
                "value must be derived from Base");
        }
    
        // moving is free thanks to unique_ptr
        polymorphic_value(polymorphic_value&&) = default;
        polymorphic_value& operator=(polymorphic_value&&) = default;
    
        // copying uses our virtual interface
        polymorphic_value(polymorphic_value const& that)
        : handle(that.handle->clone()) {}
        polymorphic_value& operator=(polymorphic_value const& that) {
            handle = that.handle->clone();
            return *this;
        }
    
        // other useful constructors involve upcasting and so on
    
        // and then useful stuff for actually using the value
        Base* operator->() { return handle.get(); }
        Base const* operator->() const { return handle.get(); }
        // ...
    
    private:
        std::unique_ptr<clonable<Base>> handle;
    };
    

    This is just a minimal interface, but it can easily be fleshed out from here to cover more usage scenarios.