Pertimbangkan penerapan daftar tertaut tunggal berikut:
struct node {
std::unique_ptr<node> next;
ComplicatedDestructorClass data;
}
Sekarang, misalkan saya berhenti menggunakan beberapa std::unique_ptr<node> head
contoh yang kemudian keluar dari ruang lingkup, menyebabkan destruktornya dipanggil.
Apakah ini akan menghancurkan tumpukan saya untuk daftar yang cukup besar? Apakah adil untuk menganggap bahwa kompiler akan melakukan optimasi yang cukup rumit (inline unique_ptr
's destructor ke node
' s, kemudian menggunakan rekursi ekor), yang menjadi lebih sulit jika saya melakukan hal berikut (karena data
destructor akan mengaburkan next
, membuat sulit bagi kompiler untuk melihat potensi pemesanan ulang dan peluang panggilan balik):
struct node {
std::shared_ptr<node> next;
ComplicatedDestructorClass data;
}
Jika data
entah bagaimana memiliki pointer ke node
maka mungkin bahkan tidak mungkin untuk rekursi ekor (meskipun tentu saja kita harus berusaha untuk menghindari pelanggaran enkapsulasi semacam itu).
Secara umum, bagaimana seharusnya seseorang menghancurkan daftar ini? Kami tidak dapat menelusuri daftar dan menghapus simpul "saat ini" karena pointer yang dibagikan tidak memiliki release
! Satu-satunya cara adalah dengan custom deleter, yang benar-benar bau bagi saya.
sumber
gcc -O3
tidak dapat mengoptimalkan rekursi ekor (dalam contoh yang rumit).Jawaban:
Ya, ini pada akhirnya akan meledakkan tumpukan Anda, kecuali jika kompiler kebetulan menerapkan optimasi panggilan ekor ke
node
destruktor danshared_ptr
destruktor. Yang terakhir ini sangat tergantung pada implementasi perpustakaan standar. STL Microsoft, misalnya, tidak akan pernah melakukan itu, karenashared_ptr
pertama-tama mengurangi jumlah referensi yang ditunjuknya (mungkin menghancurkan objek) dan kemudian mengurangi jumlah referensi blok kontrolnya (jumlah referensi yang lemah). Jadi destruktor dalam bukan panggilan ekor. Ini juga panggilan virtual , yang membuatnya semakin kecil kemungkinannya untuk dioptimalkan.Daftar tipikal mengatasi masalah itu dengan tidak memiliki satu node yang memiliki berikutnya, tetapi dengan memiliki satu wadah yang memiliki semua node, dan menggunakan loop untuk menghapus semua yang ada di destructor.
sumber
shared_ptr
yang pada akhirnya. Saya tidak bisa sepenuhnya menghilangkan pointer karena saya membutuhkan pengaman thread.std::atomic_*
kelebihan mereka, bukan?std::atomic<node*>
, dan lebih murah.Jawaban terlambat tetapi karena tidak ada yang menyediakannya ... Saya mengalami masalah yang sama dan menyelesaikannya dengan menggunakan destruktor khusus:
Jika Anda benar-benar memiliki daftar , yaitu setiap node didahului oleh satu node dan memiliki paling banyak satu pengikut, dan Anda
list
adalah pointer ke yang pertamanode
, di atas akan berfungsi.Jika Anda memiliki struktur fuzzy (misalnya grafik asiklik), Anda dapat menggunakan yang berikut:
Idenya adalah ketika Anda melakukannya:
Pointer bersama yang lama
next
dihancurkan (karenause_count
sekarang0
), dan Anda menunjuk ke berikut ini. Ini tidak persis sama dengan destruktor default, kecuali ia melakukannya berulang bukan secara rekursif dan dengan demikian menghindari stack overflow.sumber
next = std::move(next->next)
meneleponnext->~node()
secara rekursif.next->next
tidak valid (oleh operator penugasan pindah) sebelum nilai yang ditunjuk olehnext
dihancurkan, sehingga "menghentikan" rekursi. Saya benar-benar menggunakan kode ini dan pekerjaan ini (diuji dengang++
,clang
danmsvc
), tetapi sekarang setelah Anda mengatakannya, saya tidak yakin bahwa ini ditentukan oleh standar (fakta bahwa pointer yang dipindahkan tidak valid sebelum penghancuran objek lama menunjuk oleh penunjuk target).operator=(std::shared_ptr&& r)
setara denganstd::shared_ptr(std::move(r)).swap(*this)
. Masih dari standar, konstruktor pemindahanstd::shared_ptr(std::shared_ptr&& r)
maker
kosong, dengan demikianr
kosong (r.get() == nullptr
) sebelum panggilan keswap
. Dalam kasus saya, ini berartinext->next
kosong sebelum objek lama yang ditunjuknext
dihancurkan (olehswap
panggilan).f
aktifnext
, tidaknext->next
, dan karenanext->next
nol, kode itu segera berhenti.Sejujurnya saya tidak terbiasa dengan algoritma deallokasi pointer pintar dari setiap kompiler C ++, tapi saya bisa membayangkan algoritma sederhana, non-rekursif yang melakukan ini. Pertimbangkan ini:
Oleh karena itu tidak akan ada kesempatan untuk stack meluap, dan itu jauh lebih sederhana yang mengoptimalkan algoritma rekursif.
Saya tidak yakin apakah ini cocok dengan filosofi "pointer cerdas hampir nol biaya".
Saya akan menebak bahwa apa yang Anda uraikan tidak akan menyebabkan stack overflow, tetapi Anda dapat mencoba membuat eksperimen yang cerdas untuk membuktikan bahwa saya salah.
MEMPERBARUI
Nah, ini membuktikan salah apa yang saya tulis sebelumnya:
Program ini secara kekal membangun, dan mendekonstruksi rantai simpul. Itu menyebabkan stack overflow.
sumber