raw, kelemahan_ptr, unique_ptr, shared_ptr dll ... Bagaimana memilihnya dengan bijak?

33

Ada banyak petunjuk dalam C ++ tetapi jujur ​​dalam 5 tahun atau lebih dalam pemrograman C ++ (khusus dengan Qt Framework) Saya hanya menggunakan raw pointer lama:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Saya tahu ada banyak petunjuk "pintar" lainnya:

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Tetapi saya tidak memiliki gagasan sedikit pun tentang apa yang harus dilakukan dengan mereka dan apa yang dapat mereka tawarkan kepada saya dibandingkan dengan petunjuk mentah.

Misalnya saya punya header kelas ini:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Ini jelas tidak lengkap tetapi untuk masing-masing dari 3 petunjuk ini apakah boleh untuk membiarkan mereka "mentah" atau haruskah saya menggunakan sesuatu yang lebih tepat?

Dan di kedua kalinya, jika seorang majikan akan membaca kode, apakah dia akan ketat pada petunjuk apa yang saya gunakan atau tidak?

CheshireChild
sumber
Topik ini tampaknya sangat sesuai untuk SO. ini pada 2008 . Dan inilah pointer yang harus saya gunakan saat ini? . Saya yakin Anda dapat menemukan kecocokan yang lebih baik. Ini hanyalah pertama saya melihat
sehe
Imo batas ini karena ini banyak tentang makna konseptual / niat kelas-kelas ini seperti tentang rincian teknis perilaku dan implementasi mereka. Karena jawaban yang diterima condong ke yang pertama, saya senang dengan ini menjadi "versi PSE" dari pertanyaan SO.
Ixrec

Jawaban:

70

Pointer "mentah" tidak dikelola. Yaitu, baris berikut:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... akan membocorkan memori jika pendamping deletetidak dilakukan pada waktu yang tepat.

auto_ptr

Untuk meminimalkan kasus ini, std::auto_ptr<>diperkenalkan. Karena keterbatasan C ++ sebelum standar 2011, bagaimanapun, masih sangat mudah untuk auto_ptrmembocorkan memori. Akan tetapi cukup untuk kasus terbatas, seperti ini:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Salah satu kasus penggunaan terlemahnya adalah dalam wadah. Ini karena jika salinan yang auto_ptr<>dibuat dan salinan yang lama tidak diatur ulang dengan hati-hati, maka wadah dapat menghapus pointer dan kehilangan data.

unique_ptr

Sebagai pengganti, C ++ 11 memperkenalkan std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Seperti unique_ptr<>akan dibersihkan dengan benar, bahkan ketika itu melewati antara fungsi. Itu melakukan ini dengan secara semantik mewakili "kepemilikan" dari pointer - "pemilik" membersihkannya. Ini membuatnya ideal untuk digunakan dalam wadah:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

Berbeda dengan auto_ptr<>, unique_ptr<>berperilaku baik di sini, dan ketika vectormengubah ukuran, tidak ada objek yang akan terhapus secara tidak sengaja saat vectorsalinannya menjadi backing store.

shared_ptr dan weak_ptr

unique_ptr<>tentu saja berguna, tetapi ada beberapa kasus di mana Anda ingin dua bagian basis kode Anda dapat merujuk ke objek yang sama dan menyalin pointer-nya, sambil tetap dijamin pembersihan yang benar. Misalnya, pohon mungkin terlihat seperti ini, ketika menggunakan std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Dalam hal ini, kita bahkan dapat berpegang pada banyak salinan dari simpul root, dan pohon akan dibersihkan dengan benar ketika semua salinan dari simpul akar dihancurkan.

Ini berfungsi karena masing-masing shared_ptr<>memegang tidak hanya pointer ke objek, tetapi juga jumlah referensi dari semua shared_ptr<>objek yang merujuk ke pointer yang sama. Saat yang baru dibuat, hitungannya naik. Ketika seseorang dihancurkan, hitungannya turun. Saat hitungan mencapai nol, penunjuknya adalah deleted.

Jadi ini menimbulkan masalah: Struktur yang ditautkan ganda berakhir dengan referensi melingkar. Katakanlah kita ingin menambahkan parentpointer ke pohon kami Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Sekarang, jika kita menghapus a Node, ada referensi siklik untuk itu. Itu tidak akan pernah menjadi deleted karena jumlah referensi tidak akan pernah menjadi nol.

Untuk mengatasi masalah ini, Anda menggunakan std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Sekarang, semuanya akan berfungsi dengan benar, dan menghapus sebuah simpul tidak akan meninggalkan referensi yang macet ke simpul induk. Itu membuat berjalan pohon sedikit lebih rumit, namun:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

Dengan cara ini, Anda dapat mengunci referensi ke simpul tersebut, dan Anda memiliki jaminan yang masuk akal bahwa simpul itu tidak akan hilang saat Anda mengerjakannya, karena Anda memegangnya shared_ptr<>.

make_shared dan make_unique

Sekarang, ada beberapa masalah kecil dengan shared_ptr<>dan unique_ptr<>yang harus diatasi. Dua baris berikut memiliki masalah:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Jika thrower()melempar pengecualian, kedua saluran akan membocorkan memori. Dan lebih dari itu, shared_ptr<>menahan jumlah referensi jauh dari objek yang ditunjuknya dan ini bisa berarti alokasi kedua). Itu biasanya tidak diinginkan.

C ++ 11 menyediakan std::make_shared<>()dan C ++ 14 menyediakan std::make_unique<>()untuk memecahkan masalah ini:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Sekarang, dalam kedua kasus, bahkan jika thrower()melempar pengecualian, tidak akan ada kebocoran memori. Sebagai bonus, make_shared<>()memiliki kesempatan untuk membuat jumlah referensi dalam ruang memori yang sama dengan objek yang dikelola, yang keduanya dapat lebih cepat dan dapat menghemat beberapa byte memori, sambil memberikan Anda jaminan keamanan pengecualian!

Catatan tentang Qt

Perlu dicatat, bahwa Qt, yang harus mendukung kompiler pra-C ++ 11, memiliki model pengumpulan sampah sendiri: Banyak QObjectyang memiliki mekanisme di mana mereka akan dihancurkan dengan baik tanpa perlu pengguna untuk deleteitu.

Saya tidak tahu bagaimana QObjects akan berperilaku ketika dikelola oleh pointer yang dikelola C ++ 11, jadi saya tidak bisa mengatakan itu shared_ptr<QDialog>adalah ide yang bagus. Saya tidak memiliki pengalaman yang cukup dengan Qt untuk mengatakan dengan pasti, tetapi saya percaya bahwa Qt5 telah disesuaikan untuk kasus penggunaan ini.

greyfade
sumber
1
@Zators: Harap perhatikan komentar saya yang ditambahkan tentang Qt. Jawaban untuk pertanyaan Anda tentang apakah ketiga pointer harus dikelola tergantung pada apakah objek Qt akan berperilaku baik.
greyfade
2
"keduanya membuat alokasi terpisah untuk menahan pointer"? Tidak, unique_ptr tidak pernah mengalokasikan tambahan apa pun, hanya shared_ptr harus mengalokasikan referensi-hitungan + pengalokasi-objek. "kedua baris akan membocorkan memori"? tidak, hanya mungkin, bahkan tidak menjamin perilaku buruk.
Deduplicator
1
@Deduplicator: Kata-kata saya pasti tidak jelas: Ini shared_ptradalah objek yang terpisah - alokasi yang terpisah - dari newobjek ed. Mereka ada di lokasi yang berbeda. make_sharedmemiliki kemampuan untuk menempatkan mereka bersama di lokasi yang sama, yang meningkatkan lokalitas cache, antara lain.
greyfade
2
@greyfade: Nononono. shared_ptradalah sebuah objek. Dan untuk mengelola objek, ia harus mengalokasikan (referensi-hitungan (lemah + kuat) + perusak) -juga. make_sharedmemungkinkan mengalokasikan itu dan objek yang dikelola sebagai satu bagian. unique_ptrtidak menggunakan itu, jadi tidak ada keuntungan yang sesuai, selain memastikan objek selalu dimiliki oleh smart-pointer. Sebagai tambahan, seseorang dapat memiliki shared_ptryang memiliki objek yang mendasarinya dan mewakili nullptr, atau yang tidak memiliki dan mewakili non-nullpointer.
Deduplicator
1
Saya melihatnya, dan tampaknya ada kebingungan umum tentang apa yang shared_ptrdilakukan oleh: . 2. Ini berisi pointer. Kedua bagian itu independen. make_uniquedan make_sharedkeduanya memastikan objek yang dialokasikan dimasukkan dengan aman ke dalam smart-pointer. Selain itu, make_sharedmemungkinkan mengalokasikan objek kepemilikan dan pointer dikelola bersama.
Deduplicator