Perbedaan make_ shared dan normal shared_ptr di C ++

276
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

Banyak posting google dan stackoverflow ada di sini, tapi saya tidak bisa mengerti mengapa make_sharedlebih efisien daripada langsung menggunakan shared_ptr.

Dapatkah seseorang menjelaskan kepada saya langkah demi langkah urutan objek yang dibuat dan operasi yang dilakukan oleh keduanya sehingga saya akan dapat memahami bagaimana make_sharedefisien. Saya telah memberikan satu contoh di atas untuk referensi.

Anup Buchke
sumber
4
Itu tidak lebih efisien. Alasan menggunakannya adalah untuk keamanan pengecualian.
Yuushi
Beberapa artikel mengatakan, itu menghindari beberapa overhead konstruksi, dapatkah Anda menjelaskan lebih lanjut tentang ini?
Anup Buchke
16
@ Yuushi: Keamanan pengecualian adalah alasan yang baik untuk menggunakannya, tetapi juga lebih efisien.
Mike Seymour
3
32:15 adalah di mana ia memulai dalam video yang saya tautkan di atas, jika itu membantu.
chris
4
Keuntungan gaya kode minor: menggunakan make_sharedAnda dapat menulis auto p1(std::make_shared<A>())dan p1 akan memiliki jenis yang benar.
Ivan Vergiliev

Jawaban:

333

Perbedaannya adalah std::make_sharedmelakukan satu heap-alokasi, sedangkan memanggil std::shared_ptrkonstruktor melakukan dua.

Di mana alokasi tumpukan terjadi?

std::shared_ptr mengelola dua entitas:

  • blok kontrol (menyimpan data meta seperti penghitungan ulang, penghapusan tipe, dll)
  • objek yang dikelola

std::make_sharedmelakukan akuntansi heap-alokasi tunggal untuk ruang yang diperlukan untuk blok kontrol dan data. Dalam kasus lain, new Obj("foo")memanggil alokasi tumpukan untuk data yang dikelola dan std::shared_ptrkonstruktor melakukan yang lain untuk blok kontrol.

Untuk informasi lebih lanjut, lihat catatan implementasi di cppreference .

Pembaruan I: Pengecualian-Keamanan

CATATAN (2019/08/30) : Ini bukan masalah sejak C ++ 17, karena perubahan dalam urutan evaluasi argumen fungsi. Secara khusus, setiap argumen untuk suatu fungsi diperlukan untuk sepenuhnya dieksekusi sebelum evaluasi argumen lain.

Karena OP tampaknya bertanya-tanya tentang sisi pengecualian-keamanan, saya telah memperbarui jawaban saya.

Pertimbangkan contoh ini,

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

Karena C ++ memungkinkan urutan evaluasi subekspresi yang sewenang-wenang, satu kemungkinan pemesanan adalah:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

Sekarang, misalkan kita mendapatkan pengecualian yang dilemparkan pada langkah 2 (mis., Di luar memori, Rhskonstruktor melemparkan beberapa pengecualian). Kami kemudian kehilangan memori yang dialokasikan pada langkah 1, karena tidak ada yang memiliki kesempatan untuk membersihkannya. Inti dari masalah di sini adalah bahwa pointer mentah tidak segera diteruskan ke std::shared_ptrkonstruktor.

Salah satu cara untuk memperbaikinya adalah dengan melakukannya pada jalur yang terpisah sehingga pemesanan arbiter ini tidak dapat terjadi.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

Cara yang lebih disukai untuk menyelesaikan ini tentu saja adalah dengan menggunakannya std::make_shared.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

Pembaruan II: Kerugian std::make_shared

Mengutip komentar Casey :

Karena hanya ada satu alokasi, memori orang yang ditunjuk tidak dapat di-deokasi-kan sampai blok kontrol tidak lagi digunakan. A weak_ptrdapat menjaga blok kontrol tetap hidup tanpa batas.

Mengapa instance weak_ptrs menjaga blok kontrol tetap hidup?

Harus ada cara bagi weak_ptrs untuk menentukan apakah objek yang dikelola masih valid (mis. Untuk lock). Mereka melakukan ini dengan memeriksa jumlah shared_ptrs yang memiliki objek yang dikelola, yang disimpan di blok kontrol. Hasilnya adalah bahwa blok kontrol masih hidup hingga shared_ptrhitungan dan weak_ptrjumlah keduanya mencapai 0.

Kembali ke std::make_shared

Karena std::make_sharedmembuat alokasi heap tunggal untuk blok kontrol dan objek yang dikelola, tidak ada cara untuk membebaskan memori untuk blok kontrol dan objek yang dikelola secara mandiri. Kita harus menunggu sampai kita bisa membebaskan blok kontrol dan objek yang dikelola, yang kebetulan sampai tidak ada shared_ptratau weak_ptrmasih hidup.

Misalkan kita melakukan dua alokasi tumpukan untuk blok kontrol dan objek yang dikelola melalui newdan shared_ptrkonstruktor. Kemudian kita membebaskan memori untuk objek terkelola (mungkin sebelumnya) ketika tidak ada shared_ptryang hidup, dan membebaskan memori untuk blok kontrol (mungkin nanti) ketika tidak ada weak_ptryang hidup.

mpark
sumber
53
Sebaiknya Anda menyebutkan kelemahan kecil pada sudut make_sharedjuga: karena hanya ada satu alokasi, memori pointee tidak dapat dibatalkan alokasinya sampai blok kontrol tidak lagi digunakan. A weak_ptrdapat menjaga blok kontrol tetap hidup tanpa batas.
Casey
14
Poin lain, lebih gaya, adalah: Jika Anda menggunakan make_shareddan make_uniquesecara konsisten, Anda tidak akan memiliki pointer mentah dan dapat memperlakukan setiap kejadian newsebagai bau kode.
Philipp
6
Jika hanya ada satu shared_ptr, dan tidak ada weak_ptrs, menyerukan reset()pada shared_ptrcontoh akan menghapus blok kontrol. Tapi ini terlepas dari apakah make_shareddigunakan. Menggunakan make_sharedmembuat perbedaan karena dapat memperpanjang masa pakai memori yang dialokasikan untuk objek yang dikelola . Ketika shared_ptrhitungan hits 0, destruktor untuk objek yang dikelola dipanggil terlepas dari make_shared, tetapi membebaskan memori hanya dapat dilakukan jika make_sharedsedang tidak digunakan. Semoga ini membuatnya lebih jelas.
mpark
4
Juga patut disebutkan bahwa make_share dapat mengambil keuntungan dari optimasi "Kami Tahu Di Mana Anda Tinggal" yang memungkinkan blok kontrol menjadi penunjuk yang lebih kecil. (Untuk perincian, lihat presentasi GN2012 dari Stephan T. Lavavej sekitar menit ke 12.) make_share sehingga tidak hanya menghindari alokasi, tetapi juga mengalokasikan lebih sedikit memori total.
KnowItAllWannabe
1
@HannaKhalil: Apakah ini mungkin bidang yang Anda cari ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark
26

Pointer bersama mengelola objek itu sendiri, dan objek kecil yang berisi jumlah referensi dan data housekeeping lainnya. make_shareddapat mengalokasikan satu blok memori untuk menampung keduanya; membangun pointer bersama dari pointer ke objek yang sudah dialokasikan akan perlu mengalokasikan blok kedua untuk menyimpan jumlah referensi.

Selain efisiensi ini, menggunakan make_sharedberarti Anda tidak perlu berurusan dengan newdan mentah pointer sama sekali, memberikan keamanan pengecualian yang lebih baik - tidak ada kemungkinan melempar pengecualian setelah mengalokasikan objek tetapi sebelum menetapkannya ke smart pointer.

Mike Seymour
sumber
2
Saya mengerti poin pertama Anda dengan benar. Bisakah Anda menguraikan atau memberikan beberapa tautan pada poin kedua tentang keamanan pengecualian?
Anup Buchke
22

Ada kasus lain di mana dua kemungkinan berbeda, di atas yang sudah disebutkan: jika Anda perlu memanggil konstruktor non-publik (dilindungi atau pribadi), make_share mungkin tidak dapat mengaksesnya, sedangkan varian dengan karya baru baik-baik saja .

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};
Dr_Sam
sumber
Saya mengalami masalah ini dan memutuskan untuk menggunakan new, kalau tidak saya akan menggunakan make_shared. Berikut adalah pertanyaan terkait tentang itu: stackoverflow.com/questions/8147027/… .
jigglypuff
6

Jika Anda memerlukan penyelarasan memori khusus pada objek yang dikendalikan oleh shared_ptr, Anda tidak bisa mengandalkan make_share, tapi saya pikir itu satu-satunya alasan bagus untuk tidak menggunakannya.

Simon Ferquel
sumber
2
Situasi kedua di mana make_share tidak pantas adalah ketika Anda ingin menentukan deleter khusus.
KnowItAllWannabe
5

Saya melihat satu masalah dengan std :: make_share, itu tidak mendukung konstruktor pribadi / dilindungi

icebeat
sumber
3

Shared_ptr: Melakukan dua alokasi tumpukan

  1. Blok kontrol (jumlah referensi)
  2. Obyek dikelola

Make_shared: Melakukan hanya satu alokasi tumpukan

  1. Mengontrol data blok dan objek.
James
sumber
0

Tentang efisiensi dan waktu yang dihabiskan untuk alokasi, saya membuat tes sederhana di bawah ini, saya membuat banyak contoh melalui dua cara ini (satu per satu):

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

Masalahnya, menggunakan make_share membutuhkan waktu dua kali lipat dibandingkan dengan menggunakan yang baru. Jadi, menggunakan baru ada dua alokasi tumpukan bukannya satu menggunakan make_share. Mungkin ini adalah tes bodoh tapi bukankah itu menunjukkan bahwa menggunakan make_share membutuhkan lebih banyak waktu daripada menggunakan yang baru? Tentu saja, saya hanya berbicara tentang waktu yang digunakan.

orlando
sumber
4
Tes itu agak tidak ada gunanya. Apakah tes dilakukan dalam konfigurasi rilis dengan optimisasi ternyata? Juga semua barang Anda segera dibebaskan sehingga tidak realistis.
Phil1970
0

Saya pikir bagian keamanan pengecualian dari jawaban mr mpark masih menjadi perhatian yang valid. saat membuat shared_ptr seperti ini: shared_ptr <T> (T baru), T baru mungkin berhasil, sedangkan alokasi blok kontrol shared_ptr mungkin gagal. dalam skenario ini, T yang baru dialokasikan akan bocor, karena shared_ptr tidak memiliki cara untuk mengetahui bahwa itu dibuat di tempat dan aman untuk menghapusnya. atau aku melewatkan sesuatu? Saya tidak berpikir aturan yang lebih ketat pada evaluasi fungsi parameter membantu dengan cara apa pun di sini ...

Martin Vorbrodt
sumber