Salin konstruktor untuk kelas dengan unique_ptr

105

Bagaimana cara menerapkan konstruktor salinan untuk kelas yang memiliki unique_ptrvariabel anggota? Saya hanya mempertimbangkan C ++ 11.

codefx
sumber
9
Nah, apa yang Anda ingin agar dilakukan oleh pembuat salinan?
Nicol Bolas
Saya membaca bahwa unique_ptr tidak dapat disalin. Ini membuat saya bertanya-tanya bagaimana cara menggunakan kelas yang memiliki variabel anggota unique_ptr dalam file std::vector.
codefx
2
@AbhijitKadam Anda dapat membuat salinan mendalam dari konten unique_ptr. Nyatanya, itu sering kali merupakan hal yang masuk akal untuk dilakukan.
Kubik
2
Harap dicatat bahwa Anda mungkin mengajukan pertanyaan yang salah. Anda mungkin tidak menginginkan salinan konstruktor untuk kelas Anda yang berisi a unique_ptr, Anda mungkin menginginkan konstruktor pindahkan, jika tujuan Anda adalah meletakkan data dalam file std::vector. Di sisi lain, standar C ++ 11 secara otomatis membuat konstruktor pemindahan, jadi mungkin Anda memang menginginkan konstruktor salinan ...
Yakk - Adam Nevraumont
3
Elemen vektor @codefx tidak harus dapat disalin; itu hanya berarti bahwa vektor tidak dapat disalin.
MM

Jawaban:

81

Karena unique_ptrtidak dapat dibagikan, Anda perlu menyalin kontennya atau mengonversinya unique_ptrmenjadi file 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;
}

Anda dapat, seperti yang disebutkan NPE, menggunakan move-ctor, bukan copy-ctor, tetapi itu akan menghasilkan semantik yang berbeda di kelas Anda. Seorang pemindah perlu membuat anggotanya dapat dipindahkan secara eksplisit melalui std::move:

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

Memiliki satu set lengkap operator yang diperlukan juga mengarah ke

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

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

Jika Anda ingin menggunakan kelas Anda dalam a std::vector, pada dasarnya Anda harus memutuskan apakah vektor akan menjadi pemilik unik suatu objek, dalam hal ini akan cukup untuk membuat kelas dapat dipindahkan, tetapi tidak dapat disalin. Jika Anda mengabaikan copy-ctor dan copy-assignment, kompilator akan memandu cara Anda menggunakan std :: vector dengan tipe hanya-bergerak.

Daniel Frey
sumber
4
Mungkinkah layak disebut konstruktor bergerak?
NPE
4
+1, tetapi konstruktor pemindah harus lebih ditekankan. Dalam sebuah komentar, OP mengatakan tujuannya adalah menggunakan objek dalam vektor. Untuk itu, pindah konstruksi dan tugas pindah adalah satu-satunya hal yang diperlukan.
jogojapan
36
Sebagai peringatan, strategi di atas berfungsi untuk tipe sederhana seperti int. Jika Anda memiliki unique_ptr<Base>yang menyimpan a Derived, di atas akan terpotong.
Yakk - Adam Nevraumont
5
Tidak ada pemeriksaan untuk null, jadi seperti ini memungkinkan dereferensi nullptr. Bagaimana denganA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining
1
@Aaron dalam situasi polimorfik, tipe deleter akan terhapus entah bagaimana, atau tidak berguna (jika Anda tahu tipe yang akan dihapus, mengapa hanya mengubah deleter?). Dalam kasus apapun, ya, ini adalah desain sebuah value_ptr- unique_ptrditambah Deleter / informasi mesin fotokopi.
Yakk - Adam Nevraumont
47

Kasus umum bagi seseorang untuk memiliki a unique_ptrdi kelas adalah dapat menggunakan pewarisan (jika tidak, objek biasa akan sering melakukannya juga, lihat RAII). Untuk kasus ini, sampai saat ini belum ada jawaban yang sesuai di thread ini .

Jadi, inilah titik awalnya:

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

... dan tujuannya adalah, seperti yang dikatakan, membuat dapat Foodisalin.

Untuk ini, seseorang perlu melakukan salinan mendalam dari penunjuk yang ada untuk memastikan kelas turunan disalin dengan benar.

Ini dapat dilakukan dengan menambahkan kode berikut:

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

Pada dasarnya ada dua hal yang terjadi di sini:

  • Yang pertama adalah penambahan konstruktor salin dan pindahkan, yang secara implisit dihapus Foosaat konstruktor salinan unique_ptrdihapus. Konstruktor move dapat ditambahkan hanya dengan = default... yang hanya untuk memberi tahu kompiler bahwa konstruktor move biasa tidak boleh dihapus (ini berfungsi, karena unique_ptrsudah memiliki konstruktor move yang dapat digunakan dalam kasus ini).

    Untuk pembuat salinan Foo, tidak ada mekanisme yang sama karena tidak ada pembuat salinan unique_ptr. Jadi, seseorang harus membuat yang baru unique_ptr, mengisinya dengan salinan poin asli, dan menggunakannya sebagai anggota kelas yang disalin.

  • Dalam hal warisan terlibat, salinan penerima asli harus dilakukan dengan hati-hati. Alasannya adalah bahwa melakukan penyalinan sederhana melalui std::unique_ptr<Base>(*ptr)kode di atas akan menghasilkan pemotongan, yaitu, hanya komponen dasar objek yang disalin, sedangkan bagian turunannya hilang.

    Untuk menghindari hal ini, penyalinan harus dilakukan melalui pola klon. Idenya adalah untuk melakukan penyalinan melalui fungsi virtual clone_impl()yang mengembalikan a Base*di kelas dasar. Dalam kelas turunan, bagaimanapun, itu diperpanjang melalui kovarians untuk mengembalikan a Derived*, dan penunjuk ini menunjuk ke salinan yang baru dibuat dari kelas turunan. Kelas dasar kemudian dapat mengakses objek baru ini melalui penunjuk kelas dasar Base*, membungkusnya menjadi a unique_ptr, dan mengembalikannya melalui clone()fungsi aktual yang dipanggil dari luar.

davidhigh
sumber
3
Ini seharusnya jawaban yang diterima. Semua orang berputar-putar di utas ini, tanpa mengisyaratkan mengapa seseorang ingin menyalin objek yang ditunjukkan unique_ptrketika penahanan langsung akan melakukan sebaliknya. Jawabannya ??? Warisan .
Tanveer Badar
4
Seseorang mungkin menggunakan unique_ptr bahkan ketika mereka mengetahui jenis beton yang ditunjukkan karena berbagai alasan: 1. Perlu nullable. 2. Pointee sangat besar dan kami mungkin memiliki ruang stack yang terbatas. Sering (1) dan (2) akan pergi bersama-sama, maka salah satu kekuatan pada kesempatan lebih memilih unique_ptrlebih optionaluntuk jenis nullable.
Ponkadoodle
3
Idiom jerawat adalah alasan lain.
emsr
Bagaimana jika kelas dasar tidak abstrak? Membiarkannya tanpa penentu-murni dapat menyebabkan bug run-time jika Anda lupa mengimplementasikannya kembali dalam turunan.
Oleksij Plotnyc'kyj
1
@ OleksijPlotnyc'kyj: ya, jika Anda mengimplementasikan clone_implbasis dalam, kompilator tidak akan memberi tahu Anda jika Anda melupakannya di kelas turunan. Anda dapat, bagaimanapun, menggunakan kelas dasar lain Cloneabledan mengimplementasikan virtual murni di clone_implsana. Kemudian kompilator akan mengeluh jika Anda melupakannya di kelas turunan.
davidhigh
11

Coba helper ini untuk membuat salinan dalam, dan atasi ketika sumber unique_ptr adalah null.

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

Misalnya:

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

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};
Scott Langham
sumber
2
Apakah akan menyalin dengan benar jika sumber menunjuk ke sesuatu yang diturunkan dari T?
Roman Shapovalov
3
@RomanShapovalov Tidak, mungkin tidak, Anda akan mengiris. Dalam hal ini, solusinya mungkin akan menambahkan metode virtual unique_ptr <T> clone () ke tipe T Anda, dan memberikan penggantian metode clone () dalam tipe yang diturunkan dari T. Metode klon akan membuat instance baru dari jenis turunan dan mengembalikannya.
Scott Langham
Apakah tidak ada pointer unik / terbatas di c ++ atau pustaka boost yang memiliki fungsi deep copy bawaan? Alangkah baiknya jika kita tidak harus membuat konstruktor salinan kustom kita, dll. Untuk kelas yang menggunakan petunjuk cerdas ini, ketika kita menginginkan perilaku deep copy, yang sering terjadi. Hanya ingin tahu.
shadow_map
5

Daniel Frey menyebutkan tentang solusi salinan, saya akan berbicara tentang cara memindahkan 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_;
};

Mereka disebut konstruktor bergerak dan tugas pindah

Anda bisa menggunakannya seperti ini

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

}

Anda perlu membungkus a dan c dengan std :: move karena mereka memiliki nama std :: move memberi tahu kompiler untuk mengubah nilai menjadi referensi rvalue apa pun parameternya Dalam pengertian teknis, std :: move adalah analogi dengan sesuatu seperti " std :: rvalue "

Setelah dipindahkan, sumber daya unique_ptr ditransfer ke unique_ptr lain

Ada banyak topik yang mendokumentasikan rvalue referensi; ini cukup mudah untuk memulai .

Edit:

Objek yang dipindahkan akan tetap valid tetapi statusnya tidak ditentukan .

C ++ primer 5, ch13 juga memberikan penjelasan yang sangat bagus tentang bagaimana "memindahkan" objek

StereoMatching
sumber
1
jadi apa yang terjadi pada objek asetelah memanggil std :: move (a) di bkonstruktor move? Apakah itu benar-benar tidak valid?
David Doria
3

Saya sarankan gunakan 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;
}
Guyuran
sumber
-1

unique_ptr tidak dapat disalin, hanya dapat dipindahkan.

Ini secara langsung akan memengaruhi Test, yang, pada contoh kedua Anda, juga hanya dapat dipindahkan dan tidak dapat disalin.

Faktanya, ada baiknya Anda menggunakan unique_ptryang melindungi Anda dari kesalahan besar.

Misalnya, masalah utama dengan kode pertama Anda adalah bahwa penunjuk tidak pernah dihapus yang benar-benar buruk. Katakan, Anda akan memperbaikinya dengan:

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

Ini juga buruk. Apa yang terjadi, jika Anda menyalin Test? Akan ada dua kelas yang memiliki penunjuk yang menunjuk ke alamat yang sama.

Ketika salah satu Testdihancurkan, itu juga akan menghancurkan penunjuk. Saat detik Anda Testhancur, ia juga akan mencoba menghapus memori di belakang penunjuk. Tetapi itu telah dihapus dan kami akan mendapatkan beberapa kesalahan runtime akses memori yang buruk (atau perilaku tidak ditentukan jika kami tidak beruntung).

Jadi, cara yang benar adalah dengan mengimplementasikan copy constructor dan copy assignment operator, sehingga perilakunya jelas dan kita bisa membuat salinannya.

unique_ptrjauh di depan kita di sini. Ini memiliki arti semantik: " Saya unique, jadi Anda tidak bisa begitu saja menyalin saya. " Jadi, ini mencegah kita dari kesalahan sekarang menerapkan operator yang ada.

Anda dapat menentukan copy constructor dan menyalin operator penugasan untuk perilaku khusus dan kode Anda akan berfungsi. Tapi Anda, memang (!), Dipaksa untuk melakukan itu.

Moral cerita: selalu gunakan unique_ptrdalam situasi seperti ini.

Es api
sumber