Bagaimana cara meneruskan argumen unique_ptr ke konstruktor atau fungsi?

400

Saya baru saja memindahkan semantik dalam C ++ 11 dan saya tidak tahu bagaimana menangani unique_ptrparameter dalam konstruktor atau fungsi. Pertimbangkan referensi kelas ini sendiri:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

Apakah ini cara saya menulis fungsi yang mengambil unique_ptrargumen?

Dan apakah saya perlu menggunakan std::movekode panggilan?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?
codablank1
sumber
1
Bukankah ini kesalahan segmentasi saat Anda memanggil b1-> setNext pada pointer kosong?
balki

Jawaban:

836

Berikut adalah cara yang mungkin untuk mengambil pointer unik sebagai argumen, serta artinya yang terkait.

(A) Berdasarkan Nilai

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Agar pengguna dapat memanggil ini, mereka harus melakukan salah satu dari yang berikut:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Untuk mengambil pointer unik dengan nilai berarti bahwa Anda mentransfer kepemilikan pointer ke fungsi / objek / dll yang dimaksud. Setelah newBasedibangun, nextBasedijamin kosong . Anda tidak memiliki objek, dan Anda bahkan tidak memiliki pointer ke sana lagi. Itu hilang.

Ini dipastikan karena kita mengambil parameter berdasarkan nilai. std::movetidak benar-benar memindahkan apa pun; itu hanya pemain mewah. std::move(nextBase)mengembalikan sebuah Base&&yang merupakan referensi nilai r ke nextBase. Hanya itu yang dilakukannya.

Karena Base::Base(std::unique_ptr<Base> n)mengambil argumennya berdasarkan nilai daripada referensi r-nilai, C ++ akan secara otomatis membuat sementara untuk kita. Ini menciptakan std::unique_ptr<Base>dari Base&&yang kami berikan fungsi melalui std::move(nextBase). Ini adalah konstruksi sementara ini yang benar-benar memindahkan nilai dari nextBaseke argumen fungsi n.

(B) Dengan referensi nilai-non-const

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

Ini harus dipanggil pada nilai l aktual (variabel bernama). Itu tidak bisa dipanggil dengan sementara seperti ini:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

Arti ini sama dengan arti dari penggunaan lain dari referensi non-const: fungsi mungkin atau mungkin tidak mengklaim kepemilikan pointer. Diberikan kode ini:

Base newBase(nextBase);

Tidak ada jaminan yang nextBasekosong. Ini mungkin kosong; mungkin tidak. Itu sangat tergantung pada apa yang Base::Base(std::unique_ptr<Base> &n)ingin dilakukan. Karena itu, tidak begitu jelas hanya dari tanda tangan fungsi apa yang akan terjadi; Anda harus membaca implementasinya (atau dokumentasi terkait).

Karena itu, saya tidak akan menyarankan ini sebagai antarmuka.

(C) Dengan referensi nilai konstan

Base(std::unique_ptr<Base> const &n);

Saya tidak menunjukkan implementasi, karena Anda tidak dapat beralih dari a const&. Dengan melewati a const&, Anda mengatakan bahwa fungsi tersebut dapat mengakses Basemelalui pointer, tetapi tidak dapat menyimpannya di mana saja. Ia tidak dapat mengklaim kepemilikannya.

Ini bisa bermanfaat. Tidak harus untuk kasus spesifik Anda, tetapi selalu baik untuk bisa memberikan pointer kepada seseorang dan tahu bahwa mereka tidak bisa (tanpa melanggar aturan C ++, seperti tidak membuang const) mengklaim kepemilikan itu. Mereka tidak bisa menyimpannya. Mereka dapat memberikannya kepada orang lain, tetapi yang lain harus mematuhi aturan yang sama.

(D) Dengan referensi r-value

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Ini kurang lebih identik dengan kasus "oleh referensi non-konstanta-nilai". Perbedaannya adalah dua hal.

  1. Anda dapat lulus sementara:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Anda harus menggunakan std::moveketika melewati argumen non-sementara.

Yang terakhir ini benar-benar masalah. Jika Anda melihat baris ini:

Base newBase(std::move(nextBase));

Anda memiliki harapan yang masuk akal bahwa, setelah baris ini selesai, nextBaseharus kosong. Seharusnya sudah dipindahkan dari. Setelah semua, Anda memiliki yang std::moveduduk di sana, memberi tahu Anda bahwa gerakan telah terjadi.

Masalahnya adalah belum. Itu tidak dijamin telah dipindahkan dari. Ini mungkin telah dipindahkan dari, tetapi Anda hanya akan tahu dengan melihat kode sumber. Anda tidak bisa membedakan hanya dari tanda tangan fungsi.

Rekomendasi

  • (A) Berdasarkan Nilai: Jika Anda bermaksud untuk suatu fungsi untuk mengklaim kepemilikan a unique_ptr, ambil menurut nilainya.
  • (C) Dengan referensi nilai-konstan: Jika Anda bermaksud untuk suatu fungsi cukup menggunakan unique_ptruntuk durasi eksekusi fungsi itu, bawa saja const&. Atau, lewati a &atau const&ke tipe aktual yang ditunjuk, daripada menggunakan a unique_ptr.
  • (D) Berdasarkan r-value referensi: Jika suatu fungsi mungkin atau mungkin tidak mengklaim kepemilikan (tergantung pada jalur kode internal), maka bawa &&. Tetapi saya sangat menyarankan untuk tidak melakukan hal ini sedapat mungkin.

Bagaimana cara memanipulasi unique_ptr

Anda tidak dapat menyalin unique_ptr. Anda hanya bisa memindahkannya. Cara yang tepat untuk melakukan ini adalah dengan std::movefungsi perpustakaan standar.

Jika Anda mengambil unique_ptrberdasarkan nilai, Anda dapat berpindah darinya dengan bebas. Tetapi gerakan sebenarnya tidak terjadi karena std::move. Ambil pernyataan berikut:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

Ini benar-benar dua pernyataan:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(catatan: Kode di atas tidak secara teknis mengkompilasi, karena referensi r-nilai non-sementara sebenarnya tidak r-nilai. Ini di sini hanya untuk keperluan demo).

Ini temporaryhanya referensi r-nilai oldPtr. Itu adalah di konstruktor di newPtrmana gerakan terjadi. unique_ptrMove's constructor (sebuah constructor yang mengambil a &&untuk dirinya sendiri) adalah apa gerakan yang sebenarnya.

Jika Anda memiliki unique_ptrnilai dan Anda ingin menyimpannya di suatu tempat, Anda harus menggunakannya std::moveuntuk melakukan penyimpanan.

Nicol Bolas
sumber
5
@Nicol: tetapi std::movetidak menyebutkan nilai pengembaliannya. Ingat bahwa referensi nilai yang dinamai adalah nilai. ideone.com/VlEM3
R. Martinho Fernandes
31
Saya pada dasarnya setuju dengan jawaban ini, tetapi memiliki beberapa komentar. (1) Saya tidak berpikir ada use case yang valid untuk meneruskan referensi ke const lvalue: semua yang bisa dilakukan callee dengan itu, bisa juga dilakukan dengan merujuk ke pointer const (telanjang), atau bahkan lebih baik pointer itu sendiri [dan itu bukan urusannya untuk mengetahui kepemilikan dilakukan melalui unique_ptr; mungkin beberapa penelepon lain memerlukan fungsi yang sama tetapi menahan panggilan shared_ptr] (2) dengan referensi nilai dapat berguna jika fungsi yang dipanggil memodifikasi pointer, misalnya, menambah atau menghapus node (daftar yang dimiliki) dari daftar yang ditautkan.
Marc van Leeuwen
8
... (3) Meskipun argumen Anda yang mendukung lewat oleh nilai daripada melewati oleh rvalue referensi masuk akal, saya pikir bahwa standar itu sendiri selalu melewati unique_ptrnilai dengan rvalue referensi (misalnya ketika mentransformasikannya menjadi shared_ptr). Alasan untuk itu mungkin sedikit lebih efisien (tidak ada pindah ke pointer sementara dilakukan) sementara itu memberikan hak yang sama persis kepada penelepon (dapat melewati nilai, atau nilai yang dibungkus std::move, tetapi bukan nilai telanjang).
Marc van Leeuwen
19
Hanya untuk mengulangi apa yang dikatakan Marc, dan mengutip Sutter : "Jangan gunakan const unique_ptr & sebagai parameter; gunakan widget * sebagai gantinya"
Jon
17
Kami telah menemukan masalah dengan nilai-nilai - langkah ini terjadi selama inisialisasi argumen, yang tidak diurutkan sehubungan dengan evaluasi argumen lainnya (kecuali dalam initializer_list, tentu saja). Sedangkan menerima referensi nilai sangat memerintahkan perpindahan terjadi setelah pemanggilan fungsi, dan karena itu setelah evaluasi argumen lain. Jadi menerima referensi nilai harus lebih disukai setiap kali kepemilikan akan diambil.
Ben Voigt
57

Biarkan saya mencoba untuk menyatakan berbagai mode yang layak melewati pointer ke objek yang ingatannya dikelola oleh sebuah instance dari std::unique_ptrtemplat kelas; ini juga berlaku untuk std::auto_ptrtemplat kelas yang lebih lama (yang saya percaya mengizinkan semua penggunaan yang dilakukan oleh pointer unik, tetapi sebagai tambahan nilai yang dapat dimodifikasi akan diterima di mana nilai diharapkan, tanpa harus meminta std::move), dan sampai batas tertentu juga std::shared_ptr.

Sebagai contoh nyata untuk diskusi saya akan mempertimbangkan jenis daftar sederhana berikut

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

Mesin virtual dari daftar tersebut (yang tidak dapat diizinkan untuk berbagi bagian dengan mesin virtual lain atau bundar) sepenuhnya dimiliki oleh siapa pun yang memegang listpenunjuk awal . Jika kode klien tahu bahwa daftar yang disimpan tidak akan pernah kosong, itu juga dapat memilih untuk menyimpan yang pertama nodesecara langsung daripada a list. Tidak ada destruktor yang nodeperlu didefinisikan: karena destruktor untuk bidangnya secara otomatis dipanggil, seluruh daftar akan dihapus secara rekursif oleh destruktor penunjuk pintar setelah masa pakai penunjuk atau simpul awal berakhir.

Jenis rekursif ini memberikan kesempatan untuk membahas beberapa kasus yang kurang terlihat dalam kasus penunjuk pintar ke data biasa. Juga fungsi-fungsi itu sendiri kadang-kadang memberikan (secara rekursif) contoh kode klien juga. Typedef untuk listini tentu saja condong ke arah unique_ptr, tetapi definisi dapat diubah untuk menggunakan auto_ptratau shared_ptrbukannya tanpa perlu banyak mengubah apa yang dikatakan di bawah ini (terutama mengenai keamanan pengecualian yang dijamin tanpa perlu menulis destruktor).

Mode lewat pointer pintar di sekitar

Mode 0: melewati pointer atau argumen referensi alih-alih smart pointer

Jika fungsi Anda tidak terkait dengan kepemilikan, ini adalah metode yang disukai: jangan membuatnya mengambil pointer cerdas sama sekali. Dalam hal ini fungsi Anda tidak perlu khawatir siapa yang memiliki objek yang ditunjuk, atau dengan cara apa kepemilikan dikelola, sehingga melewati pointer mentah sama-sama aman, dan bentuk yang paling fleksibel, karena terlepas dari kepemilikan, klien selalu dapat menghasilkan pointer mentah (baik dengan memanggil getmetode atau dari alamat-operator &).

Misalnya fungsi untuk menghitung panjang daftar tersebut, tidak boleh memberikan listargumen, tetapi pointer mentah:

size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }

Klien yang memegang variabel list headdapat memanggil fungsi ini sebagai length(head.get()), sementara klien yang telah memilih untuk menyimpan yang node nmewakili daftar yang tidak kosong dapat memanggil length(&n).

Jika pointer dijamin bukan nol (yang tidak terjadi di sini karena daftar mungkin kosong) orang mungkin lebih suka untuk melewatkan referensi daripada pointer. Ini bisa menjadi pointer / referensi ke non- constjika fungsi perlu memperbarui konten node, tanpa menambahkan atau menghapus salah satu dari mereka (yang terakhir akan melibatkan kepemilikan).

Kasus menarik yang termasuk dalam kategori mode 0 adalah membuat salinan (dalam) daftar; sementara fungsi melakukan hal ini tentu saja harus mentransfer kepemilikan salinan yang dibuatnya, itu tidak berkaitan dengan kepemilikan daftar yang disalin. Jadi bisa didefinisikan sebagai berikut:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

Kode ini layak dicermati, baik untuk pertanyaan mengapa mengkompilasi sama sekali (hasil panggilan rekursif ke copydalam daftar penginisialisasi mengikat argumen referensi nilai dalam konstruktor bergerak unique_ptr<node>, alias list, ketika menginisialisasi nextbidang dari dihasilkan node), dan untuk pertanyaan mengapa itu adalah pengecualian-aman (jika selama proses alokasi rekursif memori habis dan beberapa panggilan newmelempar std::bad_alloc, maka pada saat itu penunjuk ke daftar yang dikonstruksi sebagian ditahan secara anonim dalam jenis sementara listdibuat untuk daftar penginisialisasi, dan destruktornya akan membersihkan daftar sebagian itu). By the way kita harus menahan godaan untuk menggantikan (sebagai awalnya saya lakukan) kedua nullptrolehp, yang setelah semua diketahui nol pada saat itu: seseorang tidak dapat membangun pointer pintar dari pointer (mentah) ke konstan , bahkan ketika itu diketahui nol.

Mode 1: melewati pointer cerdas berdasarkan nilai

Fungsi yang mengambil nilai penunjuk cerdas sebagai argumen memiliki objek yang ditunjuk langsung: penunjuk cerdas yang dipegang pemanggil (baik dalam variabel bernama atau sementara anonim) disalin ke dalam nilai argumen di pintu masuk fungsi dan pemanggil pointer menjadi nol (dalam kasus sementara salinan mungkin telah dielakkan, tetapi dalam hal apa pun penelepon telah kehilangan akses ke objek yang diarahkan ke objek). Saya ingin memanggil panggilan mode ini dengan uang tunai : penelepon membayar di muka untuk layanan yang dipanggil, dan tidak dapat memiliki ilusi tentang kepemilikan setelah panggilan. Untuk memperjelas ini, aturan bahasa mengharuskan penelepon untuk memasukkan argumenstd::movejika smart pointer disimpan dalam variabel (secara teknis, jika argumennya adalah nilai); dalam kasus ini (tetapi tidak untuk mode 3 di bawah) fungsi ini melakukan apa yang disarankan namanya, yaitu memindahkan nilai dari variabel ke sementara, meninggalkan variabel nol.

Untuk kasus-kasus di mana fungsi yang dipanggil tanpa syarat mengambil kepemilikan (pilfers) objek yang ditunjuk, mode ini digunakan dengan std::unique_ptratau std::auto_ptrmerupakan cara yang baik untuk melewatkan sebuah pointer bersama dengan kepemilikannya, yang menghindari risiko kebocoran memori. Meskipun demikian saya berpikir bahwa hanya ada beberapa situasi di mana mode 3 di bawah ini tidak disukai (sedikit pun) daripada mode 1. Untuk alasan ini saya tidak akan memberikan contoh penggunaan mode ini. (Tapi lihat reversedcontoh mode 3 di bawah ini, di mana dikatakan bahwa mode 1 akan melakukan setidaknya juga.) Jika fungsi tersebut membutuhkan lebih banyak argumen daripada hanya pointer ini, mungkin terjadi bahwa ada tambahan alasan teknis untuk menghindari mode 1 (dengan std::unique_ptratau std::auto_ptr): karena operasi pemindahan yang sebenarnya terjadi saat melewati variabel penunjukpoleh ungkapan std::move(p), tidak dapat diasumsikan pmemiliki nilai yang berguna saat mengevaluasi argumen lain (urutan evaluasi tidak ditentukan), yang dapat menyebabkan kesalahan halus; sebaliknya, menggunakan mode 3 memastikan bahwa tidak ada pemindahan dari ptempat terjadi sebelum pemanggilan fungsi, sehingga argumen lain dapat dengan aman mengakses suatu nilai p.

Ketika digunakan dengan std::shared_ptr, mode ini menarik karena dengan definisi fungsi tunggal memungkinkan pemanggil untuk memilih apakah akan menyimpan salinan berbagi pointer untuk dirinya sendiri sambil membuat salinan berbagi baru untuk digunakan oleh fungsi (ini terjadi ketika nilai lebih argumen disediakan; copy constructor untuk pointer bersama yang digunakan pada panggilan meningkatkan jumlah referensi), atau hanya memberikan fungsi salinan dari pointer tanpa mempertahankan satu atau menyentuh jumlah referensi (ini terjadi ketika argumen nilai diberikan, mungkin nilai yang dibungkus dengan panggilan std::move). Contohnya

void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container

void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null
}

Hal yang sama dapat dicapai dengan mendefinisikan secara terpisah void f(const std::shared_ptr<X>& x)(untuk kasus lvalue) dan void f(std::shared_ptr<X>&& x)(untuk kasus rvalue), dengan badan fungsi berbeda hanya dalam versi pertama memanggil semantik salinan (menggunakan konstruksi copy / penugasan saat menggunakan x) tetapi versi kedua memindahkan semantik ( std::move(x)sebagai gantinya menulis , seperti dalam kode contoh). Jadi untuk pointer bersama, mode 1 dapat berguna untuk menghindari duplikasi kode.

Mode 2: melewati smart pointer dengan referensi nilai yang dapat dimodifikasi (dapat dimodifikasi)

Di sini fungsinya hanya membutuhkan referensi yang dapat dimodifikasi untuk pointer cerdas, tetapi tidak memberikan indikasi apa yang akan dilakukan dengan itu. Saya ingin memanggil metode ini dengan kartu : penelepon memastikan pembayaran dengan memberikan nomor kartu kredit. Referensi dapat digunakan untuk mengambil kepemilikan objek runcing, tetapi tidak harus. Mode ini membutuhkan penyediaan argumen nilai yang dapat dimodifikasi, yang sesuai dengan fakta bahwa efek yang diinginkan dari fungsi tersebut termasuk meninggalkan nilai yang berguna dalam variabel argumen. Penelepon dengan ekspresi nilai yang ingin diteruskan ke fungsi seperti itu akan dipaksa untuk menyimpannya dalam variabel bernama untuk dapat melakukan panggilan, karena bahasa hanya menyediakan konversi implisit ke konstantareferensi nilai (mengacu pada sementara) dari suatu nilai. (Tidak seperti situasi sebaliknya ditangani oleh std::move, cor dari Y&&ke Y&, dengan Yjenis pointer cerdas, tidak mungkin; tetap konversi ini bisa diperoleh dengan fungsi template sederhana jika benar-benar diinginkan, lihat https://stackoverflow.com/a/24868376 / 1436796 ). Untuk kasus di mana fungsi yang dipanggil bermaksud mengambil kepemilikan objek tanpa syarat, mencuri dari argumen, kewajiban untuk memberikan argumen nilai lv memberikan sinyal yang salah: variabel tidak akan memiliki nilai berguna setelah panggilan. Karena itu mode 3, yang memberikan kemungkinan identik di dalam fungsi kita tetapi meminta penelepon untuk memberikan nilai, harus dipilih untuk penggunaan tersebut.

Namun ada kasus penggunaan yang valid untuk mode 2, yaitu fungsi yang dapat memodifikasi pointer, atau objek yang menunjuk dengan cara yang melibatkan kepemilikan . Misalnya, fungsi yang awalan sebuah simpul ke sebuah listmemberikan contoh penggunaan tersebut:

void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }

Jelas tidak diinginkan di sini untuk memaksa penelepon untuk menggunakan std::move, karena penunjuk pintar mereka masih memiliki daftar yang jelas dan tidak kosong setelah panggilan, meskipun berbeda dari sebelumnya.

Sekali lagi, menarik untuk mengamati apa yang terjadi jika prependpanggilan gagal karena kekurangan memori. Maka newpanggilan akan dibuang std::bad_alloc; pada titik waktu ini, karena tidak nodedapat dialokasikan, dapat dipastikan bahwa referensi nilai yang lewat (mode 3) dari std::move(l)belum dapat dicuri, karena itu akan dilakukan untuk membangun nextbidang nodeyang gagal dialokasikan. Jadi smart pointer asli lmasih menyimpan daftar asli ketika kesalahan dilemparkan; daftar itu akan dimusnahkan dengan baik oleh destruktor penunjuk pintar, atau jika lseandainya bertahan hidup berkat catchklausa awal yang cukup , itu masih akan memegang daftar asli.

Itu adalah contoh konstruktif; dengan mengedipkan mata ke pertanyaan ini orang juga dapat memberikan contoh yang lebih destruktif dari menghapus node pertama yang berisi nilai yang diberikan, jika ada:

void remove_first(int x, list& l)
{ list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 
}

Sekali lagi kebenarannya cukup halus di sini. Khususnya, dalam pernyataan akhir, pointer yang (*p)->nextdisimpan di dalam node yang akan dihapus tidak terhubung (oleh release, yang mengembalikan pointer tetapi membuat null asli) sebelum reset (secara implisit) menghancurkan node itu (ketika itu menghancurkan nilai lama yang dipegang oleh p), memastikan bahwa satu dan hanya satu simpul yang hancur pada saat itu. (Dalam bentuk alternatif yang disebutkan dalam komentar, waktu ini akan diserahkan kepada internal implementasi operator penugasan pindah std::unique_ptrinstance list; standar mengatakan 20.7.1.2.3; 2 bahwa operator ini harus bertindak "seolah-olah oleh memanggil reset(u.release())", kapan waktunya harus aman di sini juga.)

Perhatikan bahwa prependdan remove_firsttidak dapat dipanggil oleh klien yang menyimpan nodevariabel lokal untuk daftar yang selalu kosong, dan memang benar karena implementasi yang diberikan tidak dapat berfungsi untuk kasus-kasus seperti itu.

Mode 3: melewati pointer cerdas dengan referensi nilai (dapat dimodifikasi)

Ini adalah mode yang lebih disukai untuk digunakan ketika hanya mengambil kepemilikan pointer. Saya ingin memanggil metode ini dengan cek : penelepon harus menerima pelepasan kepemilikan, seolah-olah memberikan uang tunai, dengan menandatangani cek, tetapi penarikan yang sebenarnya ditunda hingga fungsi yang dipanggil benar-benar menusuk pointer (persis seperti ketika menggunakan mode 2) ). "Penandatanganan cek" secara konkret berarti penelepon harus membungkus argumen dalam std::move(seperti dalam mode 1) jika itu adalah nilai rendah (jika itu adalah nilai, bagian "menyerahkan kepemilikan" jelas dan tidak memerlukan kode terpisah).

Perhatikan bahwa secara teknis mode 3 berperilaku persis seperti mode 2, sehingga fungsi yang dipanggil tidak harus mengambil alih kepemilikan; Namun saya akan bersikeras bahwa jika ada ketidakpastian tentang transfer kepemilikan (dalam penggunaan normal), mode 2 harus lebih suka mode 3, sehingga menggunakan mode 3 secara implisit sinyal untuk penelepon bahwa mereka yang menyerah kepemilikan. Orang mungkin membalas bahwa hanya mode 1 argumen yang lewat benar-benar menandakan hilangnya kepemilikan oleh penelepon. Tetapi jika klien memiliki keraguan tentang niat dari fungsi yang dipanggil, dia seharusnya mengetahui spesifikasi fungsi yang dipanggil, yang seharusnya menghilangkan keraguan.

Sangatlah sulit untuk menemukan contoh khas yang melibatkan listtipe kita yang menggunakan mode 3 argumen yang lewat. Memindahkan daftar bke akhir daftar lain aadalah contoh umum; namun a(yang bertahan dan menahan hasil operasi) lebih baik dilewatkan menggunakan mode 2:

void append (list& a, list&& b)
{ list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here
}

Contoh murni dari mode 3 argumen yang lewat adalah berikut ini yang mengambil daftar (dan kepemilikannya), dan mengembalikan daftar yang berisi node identik dalam urutan terbalik.

list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
  { // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  }
  return result;
}

Fungsi ini bisa disebut sebagai l = reversed(std::move(l));untuk membalik daftar menjadi dirinya sendiri, tetapi daftar terbalik juga dapat digunakan secara berbeda.

Di sini argumen segera dipindahkan ke variabel lokal untuk efisiensi (orang bisa menggunakan parameter llangsung di tempat p, tetapi kemudian mengaksesnya setiap kali akan melibatkan tingkat tipuan ekstra); maka perbedaan dengan mode 1 argumen yang lewat adalah minimal. Sebenarnya menggunakan mode itu, argumen bisa berfungsi langsung sebagai variabel lokal, sehingga menghindari langkah awal itu; ini hanya contoh dari prinsip umum bahwa jika argumen yang dilewatkan oleh referensi hanya berfungsi untuk menginisialisasi variabel lokal, orang mungkin juga meneruskannya dengan nilai sebagai gantinya dan menggunakan parameter sebagai variabel lokal.

Menggunakan mode 3 nampaknya didukung oleh standar, seperti yang disaksikan oleh fakta bahwa semua fungsi pustaka yang disediakan yang mentransfer kepemilikan smart pointer menggunakan mode 3. Kasus meyakinkan tertentu adalah konstruktor std::shared_ptr<T>(auto_ptr<T>&& p). Konstruktor itu digunakan (dalam std::tr1) untuk mengambil referensi lvalue yang dapat dimodifikasi (sama seperti auto_ptr<T>&copy constructor), dan karenanya dapat dipanggil dengan auto_ptr<T>lvalue pseperti pada std::shared_ptr<T> q(p), setelah pitu telah disetel ulang ke nol. Karena perubahan dari mode 2 ke 3 dalam melewati argumen, kode lama ini sekarang harus ditulis ulang std::shared_ptr<T> q(std::move(p))dan kemudian akan terus bekerja. Saya mengerti bahwa panitia tidak menyukai mode 2 di sini, tetapi mereka memiliki opsi untuk mengubah ke mode 1, dengan mendefinisikanstd::shared_ptr<T>(auto_ptr<T> p)sebagai gantinya, mereka dapat memastikan bahwa kode lama berfungsi tanpa modifikasi, karena (tidak seperti pointer unik) pointer otomatis dapat secara diam-diam direferensikan ke nilai (objek pointer itu sendiri diatur ulang ke nol dalam proses). Rupanya panitia lebih menyukai advokasi mode 3 daripada mode 1, sehingga mereka memilih untuk secara aktif memecahkan kode yang ada daripada menggunakan mode 1 bahkan untuk penggunaan yang sudah usang.

Kapan memilih mode 3 daripada mode 1

Mode 1 benar-benar dapat digunakan dalam banyak kasus, dan mungkin lebih disukai daripada mode 3 dalam kasus di mana asumsi kepemilikan akan mengambil bentuk memindahkan pointer pintar ke variabel lokal seperti pada reversedcontoh di atas. Namun, saya dapat melihat dua alasan untuk memilih mode 3 dalam kasus yang lebih umum:

  • Ini sedikit lebih efisien untuk melewati referensi daripada membuat sementara dan nix pointer lama (penanganan uang tunai agak melelahkan); dalam beberapa skenario pointer mungkin dilewatkan beberapa kali tidak berubah ke fungsi lain sebelum benar-benar dicuri. Melewati seperti itu umumnya akan memerlukan penulisan std::move(kecuali mode 2 digunakan), tetapi perhatikan bahwa ini hanya pemain yang tidak benar-benar melakukan apa-apa (khususnya tanpa dereferencing), sehingga tidak ada biaya tambahan.

  • Haruskah dibayangkan bahwa apa pun melempar pengecualian antara awal panggilan fungsi dan titik di mana itu (atau beberapa panggilan yang terkandung) benar-benar memindahkan objek yang diarahkan ke ke dalam struktur data lain (dan pengecualian ini belum terperangkap di dalam fungsi itu sendiri ), maka ketika menggunakan mode 1, objek yang dirujuk oleh smart pointer akan dihancurkan sebelum catchklausa dapat menangani pengecualian (karena parameter fungsi dirusak selama stack unwinding), tetapi tidak demikian ketika menggunakan mode 3. Yang terakhir memberikan penelepon memiliki opsi untuk memulihkan data objek dalam kasus tersebut (dengan menangkap pengecualian). Perhatikan bahwa mode 1 di sini tidak menyebabkan kebocoran memori , tetapi dapat menyebabkan hilangnya data yang tidak dapat dipulihkan untuk program, yang mungkin juga tidak diinginkan.

Mengembalikan penunjuk cerdas: selalu berdasarkan nilai

Untuk menyimpulkan kata tentang mengembalikan pointer cerdas, mungkin menunjuk ke objek yang dibuat untuk digunakan oleh pemanggil. Ini sebenarnya bukan kasus yang sebanding dengan melewatkan pointer ke fungsi, tetapi untuk kelengkapan saya ingin menegaskan bahwa dalam kasus seperti itu selalu kembali dengan nilai (dan tidak digunakan std::move dalam returnpernyataan). Tidak ada yang ingin mendapatkan referensi ke pointer yang mungkin baru saja di-nixed.

Marc van Leeuwen
sumber
1
+1 untuk Mode 0 - melewati pointer yang mendasarinya dan bukan unique_ptr. Sedikit topik (karena pertanyaannya adalah tentang melewati Unique_ptr) tapi itu sederhana dan menghindari masalah.
Machta
" mode 1 di sini tidak menyebabkan kebocoran memori " - yang menyiratkan bahwa mode 3 memang menyebabkan kebocoran memori, yang tidak benar. Terlepas dari apakah unique_ptrtelah dipindahkan-dari atau tidak, itu masih akan menghapus nilai dengan baik jika masih menyimpannya setiap kali dihancurkan atau digunakan kembali.
rustyx
@RustyX: Saya tidak bisa melihat bagaimana Anda menafsirkan implikasi itu, dan saya tidak pernah bermaksud mengatakan apa yang menurut Anda tersirat. Yang saya maksudkan adalah bahwa seperti di tempat lain penggunaan unique_ptrmencegah kebocoran memori (dan dengan demikian dalam arti memenuhi kontraknya), tetapi di sini (yaitu, menggunakan mode 1) itu dapat menyebabkan (dalam keadaan tertentu) sesuatu yang mungkin dianggap lebih berbahaya , yaitu hilangnya data (penghancuran nilai yang ditunjukkan) yang bisa dihindari menggunakan mode 3.
Marc van Leeuwen
4

Ya, Anda harus melakukannya jika mengambil unique_ptrnilai dalam konstruktor. Kejelasan adalah hal yang baik. Karena tidak unique_ptrdapat disalin (private copy ctor), apa yang Anda tulis harus memberi Anda kesalahan kompiler.

Xeo
sumber
3

Sunting: Jawaban ini salah, meskipun, sebenarnya kode itu berfungsi. Saya hanya meninggalkannya di sini karena diskusi di bawahnya terlalu berguna. Jawaban lain ini adalah jawaban terbaik yang diberikan pada saat saya terakhir mengedit ini: Bagaimana cara saya melewatkan argumen unique_ptr ke konstruktor atau fungsi?

Ide dasarnya ::std::moveadalah bahwa orang-orang yang melewati Anda unique_ptrharus menggunakannya untuk mengungkapkan pengetahuan bahwa mereka tahu unique_ptrmereka lewat akan kehilangan kepemilikan.

Ini berarti Anda harus menggunakan referensi nilai untuk unique_ptrdalam metode Anda, bukan unique_ptritu sendiri. Lagipula ini tidak akan berhasil karena melewati yang lama biasa unique_ptrakan memerlukan membuat salinan, dan itu secara eksplisit dilarang untuk antarmuka unique_ptr. Cukup menarik, menggunakan referensi nilai bernama mengubahnya kembali menjadi nilai lagi, jadi Anda perlu menggunakan ::std::move di dalam metode Anda juga.

Ini berarti dua metode Anda akan terlihat seperti ini:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Maka orang yang menggunakan metode akan melakukan ini:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Seperti yang Anda lihat, ::std::moveekspresi bahwa pointer akan kehilangan kepemilikan pada titik yang paling relevan dan membantu untuk diketahui. Jika ini terjadi tanpa terlihat, akan sangat membingungkan bagi orang-orang yang menggunakan kelas Anda untuk objptrtiba - tiba kehilangan kepemilikan tanpa alasan yang jelas.

Omnifarious
sumber
2
Referensi nilai yang dinamai adalah nilai.
R. Martinho Fernandes
apakah kamu yakin itu Base fred(::std::move(objptr));dan tidak Base::UPtr fred(::std::move(objptr));?
codablank1
1
Untuk menambahkan komentar saya sebelumnya: kode ini tidak dapat dikompilasi. Anda masih perlu menggunakan std::moveimplementasi konstruktor dan metode. Dan bahkan ketika Anda melewati nilai, penelepon masih harus menggunakan std::moveuntuk melewati nilai. Perbedaan utama adalah bahwa dengan pass-by-value antarmuka membuat kepemilikan yang jelas akan hilang. Lihat komentar Nicol Bolas tentang jawaban lain.
R. Martinho Fernandes
@ codablank1: Ya. Saya mendemonstrasikan cara menggunakan konstruktor dan metode dalam basis yang mengambil referensi nilai.
Mahakuasa
@ R.MartinhoFernandes: Oh, menarik. Saya kira itu masuk akal. Saya mengharapkan Anda salah, tetapi pengujian yang sebenarnya membuktikan Anda benar. Diperbaiki sekarang
Mahakuasa
0
Base(Base::UPtr n):next(std::move(n)) {}

harus jauh lebih baik

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

dan

void setNext(Base::UPtr n)

seharusnya

void setNext(Base::UPtr&& n)

dengan tubuh yang sama.

Dan ... apa yang evtdi handle()??

Emilio Garavaglia
sumber
3
Tidak ada keuntungan dalam menggunakan std::forwarddisini: Base::UPtr&&adalah selalu jenis referensi nilai p, dan std::movedibagikan sebagai nilai p. Sudah diteruskan dengan benar.
R. Martinho Fernandes
7
Saya sangat tidak setuju. Jika suatu fungsi mengambil unique_ptrnilai, maka Anda dijamin bahwa constructor bergerak dipanggil pada nilai baru (atau hanya bahwa Anda diberi sementara). Ini memastikan bahwa unique_ptrvariabel yang dimiliki pengguna sekarang kosong . Jika Anda menerimanya &&, itu hanya akan dikosongkan jika kode Anda meminta operasi pemindahan. Cara Anda, adalah mungkin untuk variabel yang tidak harus dipindahkan dari pengguna. Yang membuat pengguna std::movetersangka dan membingungkan. Penggunaan std::moveharus selalu memastikan bahwa sesuatu telah dipindahkan .
Nicol Bolas
@NicolBolas: Anda benar. Saya akan menghapus jawaban saya karena sementara itu berfungsi, pengamatan Anda benar sekali.
Mahakuasa
0

Ke bagian atas memilih jawaban. Saya lebih suka melewati referensi nilai.

Saya mengerti apa masalah yang disebabkan oleh rvalue yang disebabkan oleh referensi. Tapi mari kita bagi masalah ini menjadi dua sisi:

  • untuk penelepon:

Saya harus menulis kode Base newBase(std::move(<lvalue>))atau Base newBase(<rvalue>).

  • untuk callee:

Penulis perpustakaan harus menjamin itu benar-benar akan memindahkan unique_ptr untuk menginisialisasi anggota jika ingin memiliki kepemilikan.

Itu saja.

Jika Anda melewati referensi nilai, itu hanya akan memanggil satu instruksi "pindah", tetapi jika lulus dengan nilai, itu adalah dua.

Yap, jika penulis perpustakaan tidak ahli tentang ini, ia mungkin tidak memindahkan unique_ptr untuk menginisialisasi anggota, tapi itu masalah penulis, bukan Anda. Apa pun yang melewati nilai atau referensi nilai, kode Anda sama!

Jika Anda sedang menulis perpustakaan, sekarang Anda tahu Anda harus menjaminnya, jadi lakukan saja, melewati referensi nilai adalah pilihan yang lebih baik daripada nilai. Klien yang menggunakan pustaka Anda hanya akan menulis kode yang sama.

Sekarang, untuk pertanyaan Anda. Bagaimana cara meneruskan argumen unique_ptr ke konstruktor atau fungsi?

Anda tahu apa pilihan terbaik.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

merito
sumber