Как подойти к копированию объектов с помощью интеллектуальных указателей в качестве атрибутов класса?

Из увеличить библиотечную документацию Я прочитал следующее:

Концептуально, умные указатели рассматриваются как владеющие объектом, на который указывают, и, таким образом, ответственный за удаление объекта, когда он больше не необходимо.

У меня очень простая проблема: я хочу использовать RAII для атрибутов указателя класса, который можно скопировать и присваивать.

Операции копирования и присваивания должны быть глубокими: каждый объект должен иметь свою собственную копию фактических данных. Кроме того, RTTI должен быть доступен для атрибутов (их тип также может быть определен во время выполнения).

Должен ли я искать реализацию умного указателя с возможностью копирования (данные небольшие, поэтому мне не нужно Копировать на запись указатели), или я передаю операцию копирования конструкторам копий моих объектов, как показано в этом ответе?

Какой смарт-указатель я могу выбрать для простого RAII класса, который можно копировать и присваивать? (Я думаю, что unique_ptr с делегированными операциями копирования/присваивания конструктору экземпляра класса и оператору присваивания сделал бы правильный выбор, но я не уверен)

Здесь псевдокод для проблемы с использованием raw-указателей, это просто описание проблемы, а не код С++:

// 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;
}

Ответ 1

Если вы принимаете некоторые требования к своим типам, это можно сделать, не требуя реализации виртуальных функций клонирования для всех типов. Особые требования заключаются в том, что типы имеют доступные конструкторы копирования, которые некоторые сочтут нежелательными из-за возможности случайного нарезки. Правильное использование дружбы может смягчить недостатки этого.

Если это приемлемо, можно это сделать, удалив производные типы под интерфейсом, который обеспечивает функции копирования:

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;
};

Этот интерфейс реализуется классом templated на самом производном типе и, следовательно, знает, как сделать обычные копии через конструктор копирования.

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;
};

Это создаст для нас функции виртуальной копии без дополнительного шаблона. Теперь мы можем построить класс с интеллектуальным указателем (например, не очень умный указатель, потому что он не дает семантики указателя, а семантику значений вместо этого).

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;
};

Это всего лишь минимальный интерфейс, но его можно легко скрыть отсюда, чтобы охватить больше сценариев использования.

Ответ 2

Немного поздно, но для будущих зрителей: есть готовая к использованию реализация в моей только для заголовка библиотеки Aurora и его учебник SmartPtr. С Aurora тривиально реализовать глубокую копию с помощью интеллектуальных указателей. Следующий код работает для любого типа копирования T:

aurora::CopiedPtr<T> first(new T);
aurora::CopiedPtr<T> second = first; // deep copy

Это делает ненужным использование The Big Three/Five, если у ваших классов есть указатели.

Ответ 3

Я никогда не слышал о готовой к использованию реализации, но вы можете просто сделать это самостоятельно.

Прежде всего, вы должны написать класс шаблона шаблона, который имеет метод виртуального клонирования, возвращая копию сохраненного объекта. А затем напишите некоторого полиморфного держателя этого класса, который будет скопирован

и не забывайте о отмеченном удалении http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Checked_delete

Ответ 4

Похоже, нужно иметь возможность сделать умный указатель, который создает новую копию объекта каждый раз, когда создается другой объект интеллектуального указателя. (Является ли эта копия "глубокой" или нет до конструктора объекта, объекты, которые вы храните, могут иметь много уровней владения, поскольку все, что мы знаем, так "глубоко" зависит от значения объекты. Главное для наших целей - то, что вы хотите что-то, что создает отдельный объект, когда интеллектуальный указатель создается с ссылкой из другого, а не просто вынимает указатель на существующий объект.)

Если я правильно понял вопрос, вам понадобится метод виртуального клонирования. Нет другого способа правильно вызвать конструктор производного класса.

struct Clonable {
  virtual ~Clonable() {}
  virtual Clonable* clone() = 0;
};
struct AutoPtrClonable {
  AutoPtrClonable(Clonable* cl=0) : obj(cl) { }
  AutoPtrClonable(const AutoPtrClonable& apc) : obj(apc.obj->clone()) { }
  ~AutoPtrClonable() { delete obj; }
  // operator->, operator*, etc
  Clonable* obj;
};

Чтобы использовать образец кода, внесите его в шаблон и т.д.

Ответ 5

У вас есть два решения (на самом деле у вас есть еще много, но они имеют для меня больше всего смысла:)):

Во-первых, вы можете использовать std::unique_ptr. Это хорошее решение, потому что оно заставляет вас иметь один экземпляр на указатель. (с использованием std::shared_ptr вместо этого будет работать, но если вы не добавите код явно, копия и назначение для shared_ptr будут "разделяться" - особенно того, чего вы хотите избежать).

Если вы используете std::unique_ptr, ваш конструктор копий и оператор присваивания должны явно глубоко копировать (используя либо виртуальный clone метод в интерфейсе pointee, либо новый оператор при вызове конструктора unique_ptr).

Во-вторых, вы можете опрокинуться. В этом нет ничего сложного, и мы говорим о небольшом классе (10-20 строк или около того).

Лично, если бы мне пришлось использовать этот класс умных указателей в одном месте, я бы использовал std:: unique_ptr. В противном случае (несколько указателей, такое же поведение) я бы опрокинул свой собственный, просто так, что мне не пришлось бы повторять глубокую копию для многих случаев (чтобы поддерживать принцип DRY).