Distruggere una lista di grandi dimensioni mi eccederà nel mio stack?

9

Considera la seguente implementazione di elenchi collegati singolarmente:

struct node {
    std::unique_ptr<node> next;
    ComplicatedDestructorClass data;
}

Ora, supponiamo che io smetta di usare qualche istanza di std::unique_ptr<node> head che poi esce dall'ambito, provocando la chiamata del suo distruttore.

Questo farà esplodere il mio stack per elenchi sufficientemente grandi? È giusto presumere che il compilatore esegua un'ottimizzazione piuttosto complicata (distruttore in-linea unique_ptr in node , quindi usi ricorsione di coda), che diventa molto più difficile se faccio quanto segue (dal momento che data il distruttore avrebbe offuscato next , rendendo difficile per il compilatore notare il potenziale riordino e l'opportunità di chiamare tail):

struct node {
    std::shared_ptr<node> next;
    ComplicatedDestructorClass data;
}

Se data ha in qualche modo un puntatore al suo node , allora potrebbe anche essere impossibile per la ricorsione in coda (anche se ovviamente dovremmo sforzarci di evitare tali violazioni dell'incapsulamento).

In generale, come si dovrebbe distruggere questa lista altrimenti? Non possiamo attraversare l'elenco ed eliminare il nodo "corrente" perché il puntatore condiviso non ha un release ! L'unico modo è con un deleter personalizzato, che è davvero maleodorante per me.

    
posta VF1 27.01.2015 - 04:19
fonte

3 risposte

6

Sì, questo alla fine farà esplodere il tuo stack, a meno che il compilatore non stia applicando una ottimizzazione di chiamata tail al distruttore node del distruttore e shared_ptr . Quest'ultimo è estremamente dipendente dall'implementazione della libreria standard. L'STL di Microsoft, ad esempio, non lo farà mai, perché shared_ptr prima decrementa il numero di riferimenti del suo punta (possibilmente distruggendo l'oggetto) e quindi decrementa il conteggio dei riferimenti del blocco di controllo (il conteggio dei riferimenti deboli). Quindi il distruttore interno non è una coda. È anche una chiamata virtuale , che rende ancora meno probabile l'ottimizzazione.

Gli elenchi tipici aggirano questo problema non avendo un nodo proprio il successivo, ma avendo un contenitore che possiede tutti i nodi e utilizza un ciclo per eliminare tutto nel distruttore.

    
risposta data 27.01.2015 - 11:58
fonte
4

Risposta tardiva ma poiché nessuno l'ha fornita ... Ho incontrato lo stesso problema e l'ho risolto usando un distruttore personalizzato:

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

Se hai davvero un elenco , cioè ogni nodo è preceduto da un nodo e ha al massimo un follower, e il tuo list è un puntatore al primo node , il precedente dovrebbe lavoro.

Se hai una struttura sfuocata (ad esempio un grafico aciclico), puoi usare quanto segue:

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

L'idea è che quando lo fai:

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

Il vecchio puntatore condiviso next viene distrutto (poiché il suo use_count è ora 0 ) e tu punti a quanto segue. Questo fa esattamente lo stesso del distruttore predefinito, tranne che lo fa iterativamente anziché ricorsivamente e quindi evita l'overflow dello stack.

    
risposta data 15.03.2016 - 14:57
fonte
1

Per essere onesti non ho familiarità con l'algoritmo di deallocazione del puntatore intelligente di qualsiasi compilatore C ++, ma posso immaginare un algoritmo semplice e non ricorsivo che faccia questo. Considera questo:

  • Hai una coda di puntatori intelligenti in attesa di deallocation.
  • Hai una funzione che prende il primo puntatore e lo rilascia, e lo ripete fino a quando la coda è vuota.
  • Se un puntatore intelligente necessita di deallocazione, viene spinto nella coda e viene richiamata la funzione precedente.

Quindi non ci sarebbe alcuna possibilità che lo stack trabocchi, ed è molto più semplice l'ottimizzazione di un algoritmo ricorsivo.

Non sono sicuro che questo si inserisca nella filosofia dei "puntatori intelligenti a costo zero"

Direi che ciò che hai descritto non causerebbe un overflow dello stack, ma potresti provare a costruire un esperimento intelligente per dimostrare che ho torto.

Aggiorna

Bene, questo dimostra che ho sbagliato quello che ho scritto in precedenza:

#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;
}

Questo programma costruisce eternamente e decostruisce una catena di nodi. Ciò causa uno stack overflow.

    
risposta data 27.01.2015 - 11:46
fonte

Leggi altre domande sui tag