Biarkan saya mencoba untuk menyatakan berbagai mode yang layak melewati pointer ke objek yang ingatannya dikelola oleh sebuah instance dari std::unique_ptr
templat kelas; ini juga berlaku untuk std::auto_ptr
templat 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 list
penunjuk awal . Jika kode klien tahu bahwa daftar yang disimpan tidak akan pernah kosong, itu juga dapat memilih untuk menyimpan yang pertama node
secara langsung daripada a list
. Tidak ada destruktor yang node
perlu 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 list
ini tentu saja condong ke arah unique_ptr
, tetapi definisi dapat diubah untuk menggunakan auto_ptr
atau shared_ptr
bukannya 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 get
metode atau dari alamat-operator &
).
Misalnya fungsi untuk menghitung panjang daftar tersebut, tidak boleh memberikan list
argumen, 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 head
dapat memanggil fungsi ini sebagai length(head.get())
, sementara klien yang telah memilih untuk menyimpan yang node n
mewakili 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- const
jika 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 copy
dalam daftar penginisialisasi mengikat argumen referensi nilai dalam konstruktor bergerak unique_ptr<node>
, alias list
, ketika menginisialisasi next
bidang dari dihasilkan node
), dan untuk pertanyaan mengapa itu adalah pengecualian-aman (jika selama proses alokasi rekursif memori habis dan beberapa panggilan new
melempar std::bad_alloc
, maka pada saat itu penunjuk ke daftar yang dikonstruksi sebagian ditahan secara anonim dalam jenis sementara list
dibuat untuk daftar penginisialisasi, dan destruktornya akan membersihkan daftar sebagian itu). By the way kita harus menahan godaan untuk menggantikan (sebagai awalnya saya lakukan) kedua nullptr
olehp
, 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::move
jika 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_ptr
atau std::auto_ptr
merupakan 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 reversed
contoh 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_ptr
atau std::auto_ptr
): karena operasi pemindahan yang sebenarnya terjadi saat melewati variabel penunjukp
oleh ungkapan std::move(p)
, tidak dapat diasumsikan p
memiliki 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 p
tempat 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 Y
jenis 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 list
memberikan 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 prepend
panggilan gagal karena kekurangan memori. Maka new
panggilan akan dibuang std::bad_alloc
; pada titik waktu ini, karena tidak node
dapat dialokasikan, dapat dipastikan bahwa referensi nilai yang lewat (mode 3) dari std::move(l)
belum dapat dicuri, karena itu akan dilakukan untuk membangun next
bidang node
yang gagal dialokasikan. Jadi smart pointer asli l
masih menyimpan daftar asli ketika kesalahan dilemparkan; daftar itu akan dimusnahkan dengan baik oleh destruktor penunjuk pintar, atau jika l
seandainya bertahan hidup berkat catch
klausa 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)->next
disimpan 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_ptr
instance 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 prepend
dan remove_first
tidak dapat dipanggil oleh klien yang menyimpan node
variabel 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 list
tipe kita yang menggunakan mode 3 argumen yang lewat. Memindahkan daftar b
ke akhir daftar lain a
adalah 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 l
langsung 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 p
seperti pada std::shared_ptr<T> q(p)
, setelah p
itu 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 reversed
contoh 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 catch
klausa 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 return
pernyataan). Tidak ada yang ingin mendapatkan referensi ke pointer yang mungkin baru saja di-nixed.