Akankah merusak daftar besar meluap tumpukan saya?

12

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> headcontoh 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 datadestructor 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 dataentah bagaimana memiliki pointer ke nodemaka 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.

VF1
sumber
1
Untuk apa nilainya, bahkan tanpa pelanggaran enkapsulasi yang disebutkan dalam kasus kedua, gcc -O3tidak dapat mengoptimalkan rekursi ekor (dalam contoh yang rumit).
VF1
1
Di sana Anda memiliki jawaban Anda: Itu mungkin meniup tumpukan Anda, jika kompiler tidak dapat mengoptimalkan rekursi pergi.
Bart van Ingen Schenau
@ BartvanIngenSchenau Saya kira itu adalah contoh lain dari masalah ini . Ini sangat memalukan, karena saya suka kebersihan smart pointer.
VF1

Jawaban:

7

Ya, ini pada akhirnya akan meledakkan tumpukan Anda, kecuali jika kompiler kebetulan menerapkan optimasi panggilan ekor ke nodedestruktor dan shared_ptr destruktor. Yang terakhir ini sangat tergantung pada implementasi perpustakaan standar. STL Microsoft, misalnya, tidak akan pernah melakukan itu, karena shared_ptrpertama-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.

Sebastian Redl
sumber
Ya, saya menerapkan algoritma penghapusan daftar "tipikal" dengan deleter khusus untuk mereka shared_ptryang pada akhirnya. Saya tidak bisa sepenuhnya menghilangkan pointer karena saya membutuhkan pengaman thread.
VF1
Saya tidak tahu objek pointer "counter" yang dibagikan akan memiliki destruktor virtual juga, saya selalu menganggap itu hanya sebuah POD yang menahan ref kuat + ref lemah + deleter ...
VF1
@ VF1 Apakah Anda yakin pointer memberi Anda keamanan utas yang Anda inginkan?
Sebastian Redl
Ya - itulah inti std::atomic_*kelebihan mereka, bukan?
VF1
Ya, tapi itu juga tidak bisa Anda capai std::atomic<node*>, dan lebih murah.
Sebastian Redl
5

Jawaban terlambat tetapi karena tidak ada yang menyediakannya ... Saya mengalami masalah yang sama dan menyelesaikannya dengan menggunakan destruktor khusus:

virtual ~node () throw () {
    while (next) {
        next = std::move(next->next);
    }
}

Jika Anda benar-benar memiliki daftar , yaitu setiap node didahului oleh satu node dan memiliki paling banyak satu pengikut, dan Anda listadalah pointer ke yang pertama node, di atas akan berfungsi.

Jika Anda memiliki struktur fuzzy (misalnya grafik asiklik), Anda dapat menggunakan yang berikut:

virtual ~node () throw () {
    while (next && next.use_count() < 2) {
        next = std::move(next->next);
    }
}

Idenya adalah ketika Anda melakukannya:

next = std::move(next->next);

Pointer bersama yang lama nextdihancurkan (karena use_countsekarang 0), 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.

Suaka
sumber
Ide yang menarik. Tidak yakin itu memenuhi persyaratan OP untuk keamanan benang, tapi tentu saja cara yang baik untuk mendekati masalah ini dalam hal lain.
Jules
Kecuali Anda membebani operator pemindahan, saya tidak yakin bagaimana pendekatan ini benar-benar menyelamatkan apa pun - dalam daftar nyata, setiap kondisi sementara akan dievaluasi paling banyak sekali, dengan next = std::move(next->next)menelepon next->~node()secara rekursif.
VF1
1
@ VF1 Ini berfungsi karena next->nexttidak valid (oleh operator penugasan pindah) sebelum nilai yang ditunjuk oleh nextdihancurkan, sehingga "menghentikan" rekursi. Saya benar-benar menggunakan kode ini dan pekerjaan ini (diuji dengan g++, clangdan msvc), 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).
Holt
@ Pembaruan VF1: Menurut standar, operator=(std::shared_ptr&& r)setara dengan std::shared_ptr(std::move(r)).swap(*this). Masih dari standar, konstruktor pemindahan std::shared_ptr(std::shared_ptr&& r)make rkosong, dengan demikian rkosong ( r.get() == nullptr) sebelum panggilan ke swap. Dalam kasus saya, ini berarti next->nextkosong sebelum objek lama yang ditunjuk nextdihancurkan (oleh swappanggilan).
Holt
1
@ VF1 Kode Anda tidak sama - Panggilan ke faktif next, tidak next->next, dan karena next->nextnol, kode itu segera berhenti.
Holt
1

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:

  • Anda memiliki antrean smart pointer yang menunggu deallokasi.
  • Anda memiliki fungsi yang mengambil pointer pertama dan membatalkan alokasi itu, dan ulangi ini sampai antrian kosong.
  • Jika smart pointer membutuhkan deallokasi, maka akan didorong ke antrian dan fungsi di atas dipanggil.

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:

#include <iostream>
#include <memory>

using namespace std;

class Node;

Node *last;
long i;

class Node
{
public:
   unique_ptr<Node> next;
   ~Node()
   {
     last->next.reset(new Node);
     last = last->next.get();
     cout << i++ << endl;
   }
};

void ignite()
{
    Node n;
    n.next.reset(new Node);
    last = n.next.get();
}

int main()
{
    i = 0;
    ignite();
    return 0;
}

Program ini secara kekal membangun, dan mendekonstruksi rantai simpul. Itu menyebabkan stack overflow.

Gábor Angyal
sumber
1
Ah, maksudmu menggunakan gaya kelanjutan-lewat? Secara efektif, itulah yang Anda gambarkan. Namun, saya lebih cepat mengorbankan pointer cerdas daripada membangun daftar lain di heap hanya untuk membatalkan alokasi yang lama.
VF1
Saya salah. Saya telah mengubah jawaban saya sesuai dengan itu.
Gábor Angyal