Конструктор копирования для класса с unique_ptr

105

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

codefx
источник
9
Что вы хотите от конструктора копирования?
Никол Болас
Я читал, что unique_ptr не копируется. Это заставляет меня задаться вопросом, как использовать класс, имеющий переменную-член unique_ptr в std::vector.
codefx
2
@AbhijitKadam Вы можете сделать полную копию содержимого unique_ptr. Фактически, это часто бывает разумным.
Cubic
2
Обратите внимание: возможно, вы задаете неправильный вопрос. Вероятно, вам не нужен конструктор копирования для вашего класса, содержащий a unique_ptr, вы, вероятно, захотите конструктор перемещения, если ваша цель - поместить данные в std::vector. С другой стороны, стандарт C ++ 11 автоматически создает конструкторы перемещения, так что, возможно, вам действительно нужен конструктор копирования ...
Якк - Адам Неврамонт
3
векторные элементы @codefx не обязательно должны быть копируемыми; это просто означает, что вектор нельзя будет скопировать.
MM

Ответы:

81

Поскольку 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,
}

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

Дэниел Фрей
источник
4
Может быть, стоит упомянуть конструкторы ходов?
NPE
4
+1, но еще больше стоит выделить конструктор ходов. В комментарии OP говорит, что цель - использовать объект в векторе. Для этого требуется только конструкция перемещения и назначение перемещения.
jogojapan
36
В качестве предупреждения приведенная выше стратегия работает для простых типов, таких как int. Если у вас есть a, в unique_ptr<Base>котором хранится a Derived, приведенное выше будет нарезано.
Якк - Адам Неврамонт
5
Здесь нет проверки на null, так что это позволяет разыменовать nullptr. Как насчетA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Райан Хейнинг
1
@Aaron в полиморфных ситуациях, тип удаления будет каким-то образом удален или будет бессмысленным (если вы знаете тип для удаления, зачем менять только удалитель?). В любом случае, да, это дизайн value_ptr- unique_ptrплюс информация об удалителе / ​​копировщике.
Yakk - Adam Nevraumont
47

Обычный случай наличия a 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
    ~Foo() = default;
    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, заполнить его копией исходного указателя и использовать его как член скопированного класса.

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

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

Дэвид Хай
источник
3
Это должен был быть принятый ответ. Все остальные ходят по кругу в этом потоке, не намекая, почему нужно копировать объект, на который указывает, unique_ptrкогда прямое сдерживание было бы иначе. Ответ??? Наследование .
Танвир Бадар
4
Можно использовать unique_ptr, даже если они знают конкретный тип, на который указывает указатель, по ряду причин: 1. Он должен допускать значение NULL. 2. Указатель очень большой, и у нас может быть ограниченное пространство стека. Часто (1) и (2) будут идти вместе, следовательно , одна мощи иногда предпочитает unique_ptrболее optionalобнуляемые тип.
Ponkadoodle
3
Идиома прыщей - еще одна причина.
emsr
Что, если базовый класс не должен быть абстрактным? Если оставить его без спецификатора pure, это может привести к ошибкам во время выполнения, если вы забудете переопределить его в производном.
олек столар
1
@ OleksijPlotnyc'kyj: да, если вы реализуете clone_implin base, компилятор не скажет вам, если вы забудете его в производном классе. Однако вы можете использовать другой базовый классCloneable и реализовать там чистый виртуальный clone_impl. Тогда компилятор пожалуется, если вы забудете это в производном классе.
davidhigh,
11

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

    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;
};
Скотт Лэнгэм
источник
2
Будет ли он правильно копироваться, если источник указывает на что-то производное от T?
Роман Шаповалов
3
@RomanShapovalov Нет, наверное, нет, получится нарезка. В этом случае решением, вероятно, было бы добавить виртуальный метод unique_ptr <T> clone () к вашему типу T и предоставить переопределения метода clone () в типах, производных от T. Метод clone создаст новый экземпляр производный тип и вернуть его.
Скотт Лэнгэм,
Нет ли уникальных указателей / указателей с ограниченной областью видимости в библиотеках C ++ или boost, которые имеют встроенную функцию глубокого копирования? Было бы неплохо не создавать наши настраиваемые конструкторы копирования и т. Д. Для классов, использующих эти интеллектуальные указатели, когда нам нужно поведение глубокого копирования, что часто имеет место. Просто интересуюсь.
shadow_map
5

Дэниел Фрей упомянул о решении для копирования, я бы рассказал о том, как переместить 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 аналогичен чему-то вроде " std :: rvalue "

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

Есть много тем, которые содержат ссылки на rvalue; это довольно просто начать .

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

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

Учебник по C ++ 5, ch13 также дает очень хорошее объяснение того, как «перемещать» объект.

StereoMatching
источник
1
так что же происходит с объектом aпосле вызова std :: move (a) в bконструкторе перемещения? Это просто совершенно недействительно?
Дэвид Дориа
3

Я предлагаю использовать 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;
}
Всплеск
источник
-1

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в подобных ситуациях.

Ледяной огонь
источник