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


105

Як мені реалізувати конструктор копій для класу, який має unique_ptrзмінну члена? Я розглядаю лише C ++ 11.


9
Ну, що ви хочете зробити конструктором копій?
Нікол Болас

Я прочитав, що unique_ptr неможливо скопіювати. Це змушує мене замислитися, як використовувати клас, який має змінну члена jedinstveного_ptr в std::vector.
codefx

2
@AbhijitKadam Ви можете зробити глибоку копію вмісту unique_ptr. Насправді, це часто розумно робити.
Кубічний

2
Зверніть увагу, що ви, можливо, задаєте неправильне запитання. Ви, мабуть, не хочете конструктор копій для вашого класу, що містить a unique_ptr, ви, мабуть, хочете конструктор переміщення, якщо вашою метою є введення даних у std::vector. З іншого боку, стандарт C ++ 11 автоматично створив конструктори переміщення, тому, можливо, ви хочете конструктор копій ...
Yakk - Adam Nevraumont

3
@codefx векторні елементи не повинні копіюватися; це просто означає, що вектор не підлягає копіюванню.
ММ

Відповіді:


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, замість копію-ctor використовувати движок 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 та присвоєння копії, компілятор буде керувати вашим способом використання std :: vector з типами, що рухаються лише для переміщення.


4
Чи варто згадати конструктори з переміщенням?
NPE

4
+1, але конструктор руху слід підкреслити ще більше. У коментарі ОП говорить, що мета - використовувати об’єкт у векторному. Для цього лише побудова руху та призначення переміщення - це єдині необхідні речі.
jogojapan

36
Як попередження, наведена вище стратегія працює для таких простих типів int. Якщо у вас є unique_ptr<Base>магазин, який зберігає Derived, вищезазначений фрагмент.
Якк - Адам Невраумон

5
Немає перевірки на null, так як - це дозволяє запускати nullptr. Як щодоA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Райан Хайнін

1
@Aaron у поліморфних ситуаціях, делетер буде видалений стираним якось або безглуздо (якщо ви знаєте тип для видалення, навіщо змінювати лише делетер?). У будь-якому випадку, так, це Конструкція value_ptr- unique_ptrплюс Deleter / інформація копір.
Якк - Адам Невраумон

46

Звичайний випадок, коли хто має 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()яка повертає a Base*в базовий клас. Однак у похідному класі він поширюється за допомогою коваріації для повернення a Derived*, і цей вказівник вказує на щойно створену копію похідного класу. Потім базовий клас може отримати доступ до цього нового об'єкта за допомогою вказівника базового класу Base*, обернути його в a unique_ptrі повернути його через фактичну clone()функцію, яка викликається ззовні.


3
Це мала бути прийнятою відповіддю. Усі інші збираються по колу в цій нитці, не натякаючи на те, чому б хотілося скопіювати об'єкт, на який вказував, unique_ptrколи пряме обмеження робило б інакше. Відповідь??? Спадщина .
Тандер Бадар

4
Можна використовувати унікальний_ptr навіть тоді, коли вони знають конкретний тип, на який вказують з різних причин: 1. Це потрібно звести нанівець. 2. Покажчик дуже великий, і ми можемо мати обмежений простір у стеці. Часто (1) і (2) будуть йти разом, отже , одна мощі іноді віддає перевагу unique_ptrбільш optionalобнуляються тип.
Ponkadoodle

3
Ідіома прищів - ще одна причина.
emsr

Що робити, якщо базовий клас не повинен бути абстрактним? Залишаючи його без чистого специфікатора, це може призвести до помилок під час запуску, якщо ви забудете повторити його у похідному.
Олексій Плотницький

1
@ OleksijPlotnyc'kyj: так, якщо ви реалізуєте clone_implбазовий, компілятор не скаже вам, якщо ви забудете його у похідному класі. Однак ви можете використовувати інший базовий клас Cloneableі реалізувати там чистий віртуальний clone_impl. Тоді компілятор скаржиться, якщо ви забудете його у похідному класі.
Давидхіг

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 Ні, напевно, ні, ви отримаєте нарізку. У цьому випадку рішенням, ймовірно, було б додати метод віртуальної унікальної_ptr <T> clone () до вашого типу T та забезпечити переосмислення методу clone () у типах, похідних від T. Метод клонування зробив би новий екземпляр похідний тип і повернути це.
Скотт Ленгем

Чи немає унікальних / масштабованих покажчиків в c ++ або збільшити бібліотеки, в яких вбудована функція глибокого копіювання? Було б непогано не створювати наші власні конструктори копій тощо для класів, які використовують ці розумні покажчики, коли ми хочемо, щоб поведінка в глибокому копіюванні, що часто буває. Просто цікаво.
shadow_map

5

Даніель Фрей згадував про рішення для копіювання, я б розповів про те, як перемістити унікальний_птр

#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 переноситься на інший унікальний_ptr

Існує багато тем, які документують посилання на оцінку; це досить просто з початку .

Редагувати:

Переміщений об'єкт залишається дійсним, але не визначеним .

C ++ праймер 5, ch13 також дають дуже хороше пояснення щодо того, як "перемістити" об'єкт


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 не підлягає копіюванню, він лише переміщується.

Це безпосередньо вплине на Тест, який, на вашу думку, також може бути переміщеним і не піддається копіюванню.

Насправді добре, що ви користуєтесь тим, 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в подібних ситуаціях.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.