Скопируйте конструктор для класса с unique_ptr

Как мне реализовать конструктор копирования для класса, который имеет unique_ptr переменная-член? Я рассматриваю только C++11.

7 ответов

Решение

Так как unique_ptr не может быть передан, вам нужно либо глубоко скопировать его содержимое или преобразовать unique_ptr к shared_ptr,

class A
{
   std::unique_ptr< int > up_;

public:
   A( int i ) : up_( new int( i ) ) {}
   A( const A& a ) : up_( new int( *a.up_ ) ) {}
};

int main()
{
   A a( 42 );
   A b = a;
}

Как упоминалось в NPE, вы можете использовать move-ctor вместо copy-ctor, но это приведет к различной семантике вашего класса. Move-ctor должен был бы сделать член как перемещаемый явно через std::move:

A( A&& a ) : up_( std::move( a.up_ ) ) {}

Наличие полного набора необходимых операторов также приводит к

A& operator=( const A& a )
{
   up_.reset( new int( *a.up_ ) );
   return *this,
}

A& operator=( A&& a )
{
   up_ = std::move( a.up_ );
   return *this,
}

Если вы хотите использовать свой класс в std::vectorвы в основном должны решить, должен ли вектор быть единственным владельцем объекта, и в этом случае будет достаточно сделать класс подвижным, но не копируемым. Если вы не укажете copy-ctor и copy-assignment, компилятор покажет вам, как использовать std::vector с типами только для перемещения.

Обычный случай для одного, чтобы иметь unique_ptr в классе должна быть возможность использовать наследование (в противном случае обычный объект также подойдет, см. RAII). Для этого случая до сих пор нет подходящего ответа в этой теме.

Итак, вот отправная точка:

struct Base
{
    //some stuff
};

struct Derived : public Base
{
    //some stuff
};

struct Foo
{
    std::unique_ptr<Base> ptr;  //points to Derived or some other derived class
};

... и цель, как сказано, сделать Foo копируемый.

Для этого нужно сделать глубокую копию содержимого указателя, чтобы убедиться, что производный класс скопирован правильно.

Это может быть достигнуто путем добавления следующего кода:

struct Base
{
    //some stuff

    auto clone() const { return std::unique_ptr<Base>(clone_impl()); }
protected:
    virtual Base* clone_impl() const = 0;
};

struct Derived : public Base
{
    //some stuff

protected:
    virtual Derived* clone_impl() const override { return new Derived(*this); };                                                 
};

struct Foo
{
    std::unique_ptr<Base> ptr;  //points to Derived or some other derived class

    //rule of five, but a user-defined dtor is not necessary due to unique_ptr
    Foo(Foo const& other) : ptr(other.ptr->clone()) {}
    Foo(Foo && other) = default;
    Foo& operator=(Foo const& other) { ptr = other.ptr->clone(); return *this; }
    Foo& operator=(Foo && other) = default;
};

Здесь в основном происходит две вещи:

  • Первый - это добавление конструкторов копирования и перемещения, которые неявно удаляются в Foo как конструктор копирования unique_ptr удален Конструктор перемещения может быть добавлен просто = default... который просто сообщает компилятору, что обычный конструктор перемещения не должен быть удален (это работает, как unique_ptr уже есть конструктор перемещения, который можно использовать в этом случае).

    Для копирования конструктора Foo, нет аналогичного механизма, так как нет конструктора копирования unique_ptr, Итак, нужно построить новый unique_ptrзаполните его копией оригинального pointee и используйте в качестве члена скопированного класса.

  • В случае наследования, копия оригинального pointee должна быть сделана тщательно. Причина в том, что делать простое копирование с помощью std::unique_ptr<Base>(*ptr) в приведенном выше коде это приведет к нарезке, т. е. копируется только базовый компонент объекта, а производная часть отсутствует.

    Чтобы избежать этого, копия должна быть сделана через шаблон клона. Идея состоит в том, чтобы сделать копию через виртуальную функцию clone_impl() который возвращает Base* в базовом классе. Однако в производном классе он расширяется через ковариацию для возврата Derived*и этот указатель указывает на вновь созданную копию производного класса. Базовый класс может затем получить доступ к этому новому объекту через указатель базового класса. Base*заверните в unique_ptrи вернуть его через фактический clone() функция, которая вызывается извне.

Попробуйте этот помощник для создания глубоких копий, и справьтесь, когда source unique_ptr имеет значение null.

    template< class T >
    std::unique_ptr<T> copy_unique(const std::unique_ptr<T>& source)
    {
        return source ? std::make_unique<T>(*source) : nullptr;
    }

Например:

class My
{
    My( const My& rhs )
        : member( copy_unique(rhs.member) )
    {
    }

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};

Даниэль Фрей упомянул о решении копирования, я бы поговорил о том, как переместить unique_ptr

#include <memory>
class A
{
  public:
    A() : a_(new int(33)) {}

    A(A &&data) : a_(std::move(data.a_))
    {
    }

    A& operator=(A &&data)
    {
      a_ = std::move(data.a_);
      return *this;
    }

  private:
    std::unique_ptr<int> a_;
};

Они называются конструктором перемещения и назначением перемещения.

вы можете использовать их как это

int main()
{
  A a;
  A b(std::move(a)); //this will call move constructor, transfer the resource of a to b

  A c;
  a = std::move(c); //this will call move assignment, transfer the resource of c to a

}

Вам нужно обернуть a и c с помощью std:: move, потому что они имеют имя std:: move, указывающее компилятору преобразовать значение в ссылку на rvalue, какими бы ни были параметры. В техническом смысле std:: move является аналогом чего-то вроде " станд:: Rvalue"

После перемещения ресурс unique_ptr переносится в другой unique_ptr

Есть много тем, которые документируют ссылки; это довольно легко для начала.

Редактировать:

Перемещенный объект должен оставаться в действительном, но неопределенном состоянии.

C++ primer 5, ch13 также дают очень хорошее объяснение о том, как "перемещать" объект

Я предлагаю использовать make_unique

class A
{
   std::unique_ptr< int > up_;

public:
   A( int i ) : up_(std::make_unique<int>(i)) {}
   A( const A& a ) : up_(std::make_unique<int>(*a.up_)) {};

int main()
{
   A a( 42 );
   A b = a;
}

Как упомянул Давдихай в своем ответе . Один использует в основном из-за наследования. Таким образом, в большинстве случаев вместо (гдеclass ChildType : public BaseType {}). Однако для того, чтобы создатьstd::unique_ptr<BaseType>, всегда в какой-то момент принимает непосредственное участие. Вы либо передаете указатель на , либоunique_ptr<ChildType>в конструктор.

Бывают ситуации, когда проще переключить используемый класс, чем добавлять интерфейс клона ко всем базовым и дочерним типам, которые будут использоваться в какой-то момент.

Это позволяет написать обертку вокругunique_ptrс помощью метода клонирования (аналогично уже существующему пользовательскому средству удаления), например:

      template<typename T>
requires (std::has_virtual_destructor_v<T>)
class clonable_unique_ptr {

public: // Associated types
    using pointer = std::unique_ptr<T>::pointer;
    using element_type = std::unique_ptr<T>::element_type;
    using cloner_type = std::function<T*(const T*)>;

private:
    std::unique_ptr<T> ptr;
    cloner_type cloneFn = [](const T*) { return nullptr; };

    clonable_unique_ptr(T* rawPtr, cloner_type cloneFn)
        : ptr(rawPtr), cloneFn(cloneFn) {}

public:
    constexpr clonable_unique_ptr() noexcept {}
    constexpr clonable_unique_ptr(std::nullptr_t) noexcept {}

    /**
     * ctor creating a clonable_unique_ptr from a given rawPtr.
     * @attention The rawPtr must be of the type that it actually points to!
     *            Meaning: The ChildType
     */
    template<typename U>
    explicit clonable_unique_ptr(U* rawPtr) : ptr(rawPtr) {
        static_assert(std::derived_from<U, T>, "Types not compatible");
        static_assert(!std::is_abstract_v<U>, "Entrypoint ctors need to be given the actual type, not the base type!");
        static_assert(std::is_copy_constructible_v<U>, "Type needs to be copy constructible");
        cloneFn = [](const T* ptr) { return new U(*static_cast<const U*>(ptr)); };
    }

    /**
     * ctor creating a clonable_unique_ptr from a given unique_ptr<ChildType>.
     * @attention The ptr must be of the type that it actually points to!
     *            Meaning: The ChildType
     */
    template<typename U>
    clonable_unique_ptr(std::unique_ptr<U>&& o) {
        static_assert(std::derived_from<U, T>, "Types not compatible");
        static_assert(!std::is_abstract_v<U>, "Entrypoint ctors need to be given the actual type, not the base type!");
        static_assert(std::is_copy_constructible_v<U>, "Type needs to be copy constructible");
        cloneFn = [](const T* ptr) { return new U(*static_cast<const U*>(ptr)); };
        ptr = std::move(o);
    }

    /**
     * move ctor, moving from another instance of clonable_unique_ptr
     */
    clonable_unique_ptr(clonable_unique_ptr&& o)
        : ptr(std::move(o.ptr)), cloneFn(std::move(o.cloneFn)) {}

    /** move assignable */
    clonable_unique_ptr& operator=(clonable_unique_ptr&&) = default;


    // default unique_ptr interface with accessors
    T* get() const noexcept { return ptr.get(); }
    T* operator->() { return ptr.get(); }
    auto& operator*() const noexcept(noexcept(*std::declval<pointer>())) {
        return *ptr;
    }
    void reset() noexcept {
        ptr.reset();
        cloneFn = [](const T*) { return nullptr; };
    }
    void swap(clonable_unique_ptr& o) noexcept {
        ptr.swap(o.ptr);
        std::swap(cloneFn, o.cloneFn);
    }

    /**
     * Clone the instance of T contained in this pointer by using the copy ctor
     * of the ChildType that is contained in this ptr.
     */
    clonable_unique_ptr<T> clone() const {
        return clonable_unique_ptr(cloneFn(get()), cloneFn);
    }
};


template<typename T0, typename T1>
bool inline operator==(const clonable_unique_ptr<T0>& ptr0, const clonable_unique_ptr<T1>& ptr1) {
    return ptr0.get() == ptr1.get();
}
template<typename T0>
bool inline operator==(const clonable_unique_ptr<T0>& ptr0, std::nullptr_t) {
    return ptr0.get() == nullptr;
}

Таким образом, ответственность за реализациюcloneбыл помещен в указатель и выведен из BaseClass/ChildClass. Некоторые примеры:

      struct BaseClass {
    virtual void doStuff() = 0;
    virtual ~BaseClass() {}
};

struct ChildClass : public BaseClass {
    std::string value;
    ChildClass(std::string value) : value(value) {}
    void doStuff() override {
        std::cout << "doStuff: " << value << " " << this << std::endl;
    }
};

The clonable_unique_ptrзатем может быть создан с использованием указателя наChildType:

      clonable_unique_ptr<BaseClass> fromPtr(new ChildClass("cake"));
clonable_unique_ptr<BaseClass> fromPtrClone = fromPtr.clone();
fromPtr->doStuff(); // two different instances
fromPtrClone->doStuff();

или изstd::unique_ptr<ChildType>для облегчения совместимости:

      clonable_unique_ptr<BaseClass> fromUPtr = std::make_unique<ChildClass>("blub");
clonable_unique_ptr<BaseClass> fromUPtrClone = fromUPtr.clone();
fromUPtr->doStuff(); // two different instances
fromUPtrClone->doStuff();

Вот ссылка на Godbolt , чтобы поиграть с ним. Я еще не добавил в эту оболочку поддержку специального удаления, потому что лично мне это не нужно, но это должно быть тривиально.

unique_ptr не копируется, это только подвижно.

Это напрямую повлияет на Test, который во втором примере также может быть перемещаемым и не копируемым.

На самом деле, это хорошо, что вы используете unique_ptr который защищает вас от большой ошибки.

Например, основная проблема с вашим первым кодом заключается в том, что указатель никогда не удаляется, что очень, очень плохо. Скажем, вы бы это исправили:

class Test
{
    int* ptr; // writing this in one line is meh, not sure if even standard C++

    Test() : ptr(new int(10)) {}
    ~Test() {delete ptr;}
};

int main()
{       
     Test o;
     Test t = o;
}

Это тоже плохо. Что произойдет, если вы копируете Test? Будет два класса, у которых есть указатель, указывающий на один и тот же адрес.

Когда один Test уничтожен, он также уничтожит указатель. Когда твой второй Test уничтожен, он также попытается удалить память за указателем. Но он уже был удален, и мы получим некоторую ошибку времени доступа к памяти (или неопределенное поведение, если нам не повезет).

Таким образом, правильный путь - это реализовать конструктор копирования и оператор присваивания копии, чтобы поведение было ясным, и мы могли создать копию.

unique_ptr это далеко впереди нас здесь. Это имеет смысловой смысл: " Я есть unique так что вы не можете просто скопировать меня. "Таким образом, это предотвращает нас от ошибки при реализации операторов под рукой.

Вы можете определить конструктор копирования и оператор назначения копирования для особого поведения, и ваш код будет работать. Но вы по праву (!) Вынуждены это сделать.

Мораль этой истории: всегда используйте unique_ptr в таких ситуациях.

Другие вопросы по тегам