Apa kegunaan `enable_shared_from_this`?

349

Saya berlari menyeberang enable_shared_from_thissambil membaca contoh Boost.Asio dan setelah membaca dokumentasi saya masih bingung bagaimana ini harus digunakan dengan benar. Dapatkah seseorang tolong beri saya contoh dan penjelasan tentang kapan menggunakan kelas ini masuk akal.

fido
sumber

Jawaban:

362

Ini memungkinkan Anda untuk mendapatkan shared_ptrcontoh yang valid untuk this, ketika semua yang Anda miliki adalah this. Tanpa itu, Anda akan memiliki cara untuk mendapatkan shared_ptruntuk this, kecuali jika Anda sudah memiliki satu sebagai anggota. Contoh ini dari dokumentasi boost untuk enable_shared_from_ini :

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_from_this();
    }
}

int main()
{
    shared_ptr<Y> p(new Y);
    shared_ptr<Y> q = p->f();
    assert(p == q);
    assert(!(p < q || q < p)); // p and q must share ownership
}

Metode f()mengembalikan yang valid shared_ptr, meskipun tidak memiliki turunan anggota. Perhatikan bahwa Anda tidak bisa begitu saja melakukan ini:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

Pointer bersama yang dikembalikan ini akan memiliki jumlah referensi yang berbeda dari yang "tepat", dan salah satu dari mereka akan kehilangan dan menahan referensi yang menggantung ketika objek dihapus.

enable_shared_from_thistelah menjadi bagian dari standar C ++ 11. Anda juga bisa mendapatkannya dari sana juga dari boost.

INFORMASI 1800
sumber
202
+1. Poin kuncinya adalah bahwa teknik "jelas" hanya mengembalikan shared_ptr <Y> (ini) rusak, karena ini akhirnya membuat beberapa objek shared_ptr berbeda dengan jumlah referensi terpisah. Karena alasan ini, Anda tidak boleh membuat lebih dari satu shared_ptr dari pointer mentah yang sama .
j_random_hacker
3
Perlu dicatat bahwa dalam C ++ 11 dan yang lebih baru , sangat valid untuk menggunakan std::shared_ptrkonstruktor pada pointer mentah jika diwarisi dari std::enable_shared_from_this. Saya tidak tahu apakah semantik Boost diperbarui untuk mendukung ini.
Matius
6
@MatthewHolder Apakah Anda memiliki penawaran untuk ini? Pada cppreference.com saya membaca "Membangun std::shared_ptruntuk objek yang sudah dikelola oleh orang lain std::shared_ptrtidak akan berkonsultasi dengan referensi lemah yang disimpan secara internal dan dengan demikian akan menyebabkan perilaku yang tidak terdefinisi." ( en.cppreference.com/w/cpp/memory/enable_shared_from_this )
Thorbjørn Lindeijer
5
Mengapa Anda tidak bisa begitu saja melakukannya shared_ptr<Y> q = p?
Dan M.
2
@ ThorbjørnLindeijer, Anda benar, ini C ++ 17 dan yang lebih baru. Beberapa implementasi memang mengikuti semantik C ++ 16 sebelum dirilis. Penanganan yang tepat untuk C ++ 11 hingga C ++ 14 harus digunakan std::make_shared<T>.
Matius
198

dari artikel Dr Dobbs tentang pointer lemah, saya pikir contoh ini lebih mudah dimengerti (sumber: http://drdobbs.com/cpp/184402026 ):

... kode seperti ini tidak akan berfungsi dengan benar:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

Tak satu pun dari dua shared_ptrobjek tahu tentang yang lain, sehingga keduanya akan mencoba melepaskan sumber daya ketika mereka dihancurkan. Itu biasanya mengarah pada masalah.

Demikian pula, jika fungsi anggota memerlukan shared_ptrobjek yang memiliki objek yang dipanggil, ia tidak bisa hanya membuat objek dengan cepat:

struct S
{
  shared_ptr<S> dangerous()
  {
     return shared_ptr<S>(this);   // don't do this!
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->dangerous();
   return 0;
}

Kode ini memiliki masalah yang sama dengan contoh sebelumnya, meskipun dalam bentuk yang lebih halus. Ketika dibangun, shared_ptobjek r sp1memiliki sumber daya yang baru dialokasikan. Kode di dalam fungsi anggota S::dangeroustidak tahu tentang shared_ptrobjek itu, jadi shared_ptrobjek yang dikembalikannya berbeda sp1. Menyalin shared_ptrobjek baru ke sp2tidak membantu; ketika sp2keluar dari ruang lingkup, itu akan melepaskan sumber daya, dan ketika sp1keluar dari ruang lingkup, itu akan melepaskan sumber daya lagi.

Cara untuk menghindari masalah ini adalah dengan menggunakan templat kelas enable_shared_from_this. Templat mengambil satu argumen jenis templat, yang merupakan nama kelas yang menentukan sumber daya yang dikelola. Kelas itu harus, pada gilirannya, diturunkan secara publik dari templat; seperti ini:

struct S : enable_shared_from_this<S>
{
  shared_ptr<S> not_dangerous()
  {
    return shared_from_this();
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->not_dangerous();
   return 0;
}

Ketika Anda melakukan ini, perlu diingat bahwa objek yang Anda panggil shared_from_thisharus dimiliki oleh suatu shared_ptrobjek. Ini tidak akan berfungsi:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}
Artashes Aghajanyan
sumber
15
Terima kasih, ini menggambarkan masalah yang diselesaikan lebih baik daripada jawaban yang saat ini diterima.
goertzenator
2
+1: Jawaban yang bagus. Sebagai tambahan, alih-alih shared_ptr<S> sp1(new S);lebih disukai untuk digunakan shared_ptr<S> sp1 = make_shared<S>();, lihat misalnya stackoverflow.com/questions/18301511/…
Arun
4
Saya cukup yakin baris terakhir harus dibaca shared_ptr<S> sp2 = p->not_dangerous();karena perangkap di sini adalah Anda harus membuat shared_ptr dengan cara yang normal sebelum Anda menelepon shared_from_this()pertama kali! Ini sangat mudah salah! Sebelum C ++ 17, ini adalah UB untuk memanggil shared_from_this()sebelum tepat satu shared_ptr telah dibuat dengan cara biasa: auto sptr = std::make_shared<S>();atau shared_ptr<S> sptr(new S());. Untungnya dari C ++ 17 dan seterusnya akan melempar.
AnorZaken
2
Contoh BAD: S* s = new S(); shared_ptr<S> ptr = s->not_dangerous();<- Diijinkan untuk memanggil shared_from_ini hanya pada objek yang dibagikan sebelumnya, yaitu pada objek yang dikelola oleh std :: shared_ptr <T>. Kalau tidak, perilaku tidak terdefinisi (sampai C ++ 17) std :: bad_weak_ptr dilemparkan (oleh constructor shared_ptr dari yang dibangun-lemah, lemah_ini) (sejak C ++ 17). . Jadi kenyataannya adalah bahwa itu harus dipanggil always_dangerous(), karena Anda perlu pengetahuan apakah sudah dibagikan atau belum.
AnorZaken
2
@AnorZaken Poin bagus. Itu akan berguna jika Anda telah mengirimkan permintaan edit untuk melakukan perbaikan itu. Saya baru saja melakukannya. Hal lain yang bermanfaat adalah bagi pengirim poster untuk tidak memilih nama metode yang subjektif dan peka konteks!
underscore_d
30

Inilah penjelasan saya, dari perspektif mur dan baut (jawaban teratas tidak 'klik' dengan saya). * Perhatikan bahwa ini adalah hasil dari penyelidikan sumber untuk shared_ptr dan enable_shared_from_ini yang datang dengan Visual Studio 2012. Mungkin kompiler lain mengimplementasikan enable_shared_from_this berbeda ... *

enable_shared_from_this<T>menambahkan weak_ptr<T>contoh pribadi Tyang memegang ' satu hitungan referensi yang benar ' untuk contoh T.

Jadi, ketika Anda pertama kali membuat a shared_ptr<T>ke T * baru, itu melemah_ptr internal T * akan diinisialisasi dengan refcount dari 1. Yang baru shared_ptrpada dasarnya mendukung ini weak_ptr.

Tkemudian dapat, dalam metodenya, panggilan shared_from_thisuntuk mendapatkan sebuah instance dari shared_ptr<T>yang mendukung jumlah referensi yang disimpan secara internal yang sama . Dengan cara ini, Anda selalu memiliki satu tempat menyimpan T*penghitungan ulang alih-alih memiliki banyak shared_ptrcontoh yang tidak saling mengenal, dan masing-masing menganggap mereka shared_ptryang bertanggung jawab menghitung ulang Tdan menghapusnya saat ref -hitung mencapai nol.

Mackenir
sumber
1
Ini benar, dan bagian yang sangat penting adalah So, when you first create...karena itu adalah persyaratan (seperti yang Anda katakan lemah_ptr tidak diinisialisasi sampai Anda meneruskan pointer objek ke dalam ctor shared_ptr!) Dan persyaratan ini adalah di mana segala sesuatu bisa menjadi sangat salah jika Anda kurang teliti. Jika Anda tidak membuat shared_ptr sebelum memanggil shared_from_thisAnda mendapatkan UB - demikian juga jika Anda membuat lebih dari satu shared_ptr, Anda juga mendapatkan UB. Anda harus entah bagaimana memastikan Anda membuat shared_ptr tepat sekali.
AnorZaken
2
Dengan kata lain seluruh ide enable_shared_from_thisrapuh untuk memulai karena intinya adalah untuk bisa mendapatkan shared_ptr<T>dari T*, tetapi pada kenyataannya ketika Anda mendapatkan pointer T* t, umumnya tidak aman untuk menganggap apa pun tentang itu sudah dibagikan atau tidak, dan membuat tebakan yang salah adalah UB.
AnorZaken
" internal lemah_ptr akan diinisialisasi dengan refcount dari 1 " ptr lemah ke T tidak memiliki ptr pintar untuk T. Ptr lemah adalah ref pintar yang memiliki informasi yang cukup untuk membuat ptr kepemilikan yang merupakan "salinan" dari ptr yang memiliki lainnya. Ptr lemah tidak memiliki jumlah referensi. Ia memiliki akses ke hitungan ref, seperti semua yang memiliki ref.
curiousguy
3

Perhatikan bahwa menggunakan boost :: intrusive_ptr tidak mengalami masalah ini. Ini sering merupakan cara yang lebih mudah untuk mengatasi masalah ini.

blais
sumber
Ya, tetapi enable_shared_from_thismemungkinkan Anda untuk bekerja dengan API yang secara khusus menerima shared_ptr<>. Menurut pendapat saya, API seperti itu biasanya Doing It Wrong (karena lebih baik membiarkan sesuatu yang lebih tinggi di stack memiliki memori) tetapi jika Anda dipaksa untuk bekerja dengan API seperti itu, ini adalah pilihan yang baik.
cdunn2001
2
Lebih baik tetap dalam standar sebanyak yang Anda bisa.
Sergei
3

Ini persis sama di c ++ 11 dan yang lebih baru: Ini untuk mengaktifkan kemampuan untuk kembali thissebagai pointer bersama karena thismemberi Anda pointer mentah.

dengan kata lain, ini memungkinkan Anda untuk mengubah kode seperti ini

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

dalam hal ini:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           
mchiasson
sumber
Ini hanya akan berfungsi jika objek ini selalu dikelola oleh a shared_ptr. Anda mungkin ingin mengubah antarmuka untuk memastikan itu terjadi.
curiousguy
1
Anda benar sekali @curiousguy. Ini tidak perlu dikatakan lagi. Saya juga suka mengetik semua shared_ptr saya untuk meningkatkan keterbacaan saat mendefinisikan API publik saya. Sebagai contoh, alih-alih std::shared_ptr<Node> getParent const(), saya biasanya akan mengeksposnya sebagai NodePtr getParent const()gantinya. Jika Anda benar-benar membutuhkan akses ke pointer mentah internal (contoh terbaik: berurusan dengan perpustakaan C), ada std::shared_ptr<T>::getuntuk itu, yang saya benci sebutkan karena saya sudah accessor pointer mentah ini digunakan terlalu banyak kali untuk alasan yang salah.
mchiasson
-3

Cara lain adalah menambahkan weak_ptr<Y> m_stub anggota ke dalam class Y. Lalu menulis:

shared_ptr<Y> Y::f()
{
    return m_stub.lock();
}

Berguna saat Anda tidak dapat mengubah kelas tempat Anda berasal (misalnya memperluas perpustakaan orang lain). Jangan lupa untuk menginisialisasi anggota, misalnya dengan m_stub = shared_ptr<Y>(this), ini berlaku bahkan selama konstruktor.

Tidak apa-apa jika ada lebih banyak bertopik seperti ini dalam hierarki warisan, itu tidak akan mencegah penghancuran objek.

Sunting: Seperti yang ditunjukkan dengan benar oleh pengguna nobar, kode akan menghancurkan objek Y ketika tugas selesai dan variabel sementara dihancurkan. Karena itu jawaban saya salah.

PetrH
sumber
4
Jika niat Anda di sini adalah untuk menghasilkan sesuatu shared_ptr<>yang tidak menghapus poinnya, ini berlebihan. Anda bisa mengatakan di return shared_ptr<Y>(this, no_op_deleter);mana no_op_deleterobjek fungsi unary mengambil Y*dan tidak melakukan apa pun.
John Zwinck
2
Tampaknya tidak mungkin bahwa ini adalah solusi yang berfungsi. m_stub = shared_ptr<Y>(this)akan membuat dan segera merusak shared_ptr sementara dari ini. Ketika pernyataan ini selesai, thisakan dihapus dan semua referensi selanjutnya akan menjuntai.
nobar
2
Penulis mengakui jawaban ini salah sehingga ia mungkin bisa menghapusnya. Tapi dia masuk terakhir 4,5 tahun jadi tidak mungkin melakukannya - bisakah seseorang dengan kekuatan yang lebih tinggi menghilangkan ikan haring merah ini?
Tom Goodfellow
jika Anda melihat implementasi enable_shared_from_this, itu membuat itu weak_ptrsendiri (diisi oleh ctor), dikembalikan sebagai shared_ptrsaat Anda menelepon shared_from_this. Dengan kata lain, Anda menduplikasi apa yang enable_shared_from_thissudah disediakan.
mchiasson