In C ++, è un riflesso del design del software scadente se gli oggetti vengono cancellati manualmente?

6

Con l'avvento dei puntatori intelligenti, è un segno di design scadente se vedo oggetti cancellati? Sto vedendo alcuni componenti software nel nostro prodotto che la gente sta ancora facendo questo. Questa pratica mi sembra poco idiomatica, ma devo essere sicuro che questo sia il consenso del settore. Non sto iniziando una crociata, ma sarebbe bello essere preparati alla teoria.

Modifica : usi legittimi dell'eliminazione, Klaim menziona il caso d'uso del pool di oggetti. Sono d'accordo.

Cattivi esempi di utilizzo di delete, sto vedendo molti nuovi in constructor o start () e corrispondenti delete nel distruttore o stop (), perché non usare scoped_ptr? Rende il codice più pulito.

    
posta grokus 13.03.2011 - 17:58
fonte

6 risposte

9

Risposta breve: dipende e l'uso sistematico di puntatori intelligenti è semplicemente sbagliato . Pensa prima. Sto utilizzando i puntatori intelligenti per un sacco di cose, ma non è giusto per tutto, ad es. nessun proiettile d'argento. Dovrai capire la tua implementazione specifica per capire se è sbagliato o meno. Sto dando alcuni esempi nel ...

Risposta lunga:

Ciò che rende un software scarso, per quanto riguarda la durata dell'oggetto, è solo la mancanza di un controllo chiaro e preciso.

C ++ che consente di definire la durata degli oggetti significa che i programmatori devono impostare i modi per gestire tali vite, quanto possono essere diversi e quanto è facile cambiarli.

Conosco molti casi in cui i puntatori intelligenti sono solo la risposta sbagliata (o eccessiva), a cominciare dagli oggetti nei pool. Se gli oggetti sono gestiti all'interno di un oggetto "master" che eseguirà il nuovo e l'elimina chiamati in un modo isolato, allora va bene. Non dimenticare che smart_pointers, come qualsiasi altra tecnica, nascondono solo le eliminazioni in modo gestibile. Per ottenere ciò, si rendono chiari quando l'eliminazione verrà chiamata e ne farà una regola.

Quindi, l'idea qui è che fino a quando la chiamata di eliminazione è messa in un posto, facile da trovare, facile da capire, ecc. e che è ovvio che le persone che hanno scritto il codice volevano le regole per eliminare l'oggetto per essere uniformi (nessuna cancellazione nascosta in un codice "caso speciale"), quindi non è un software scadente.

I puntatori intelligenti sono pensati per essere la "risposta facile" a una serie di casi in cui non puoi essere sicuro di dove debba essere effettuata la chiamata di eliminazione. Quindi devi definire come eliminarlo e definire una regola che attiva questa eliminazione. I puntatori condivisi eliminano una volta che non vi è alcun riferimento all'oggetto. I puntatori individuati eliminano una volta fuori dall'ambito. ecc. È facile da usare e risolvere molti casi.

Ma come ogni strumento, non è una pallottola d'argento. Come detto in precedenza, non è possibile fornire puntatori intelligenti per oggetti allocati nei pool. Nei videogiochi, spesso "conosci" esattamente la quantità di oggetti di ogni tipo consentiti allo stesso tempo e la frequenza di creazione / distruzione di tali oggetti. Allora perché fare di nuovo e cancellare in questo caso? Hai solo bisogno di nuovi tutti gli oggetti nella memoria grezza, usarli e cancellare tutto alla fine, o semplicemente scaricare la memoria non elaborata.

In effetti, quasi tutte le scelte in questi casi sono guidate dall'hardware, dalla sicurezza o da altri vincoli.

Non ci sono regole dure e veloci, solo buone soluzioni a problemi specifici. Soprattutto in C ++ come tu sei il responsabile, non una VM.

Se ritieni che un codice abbia un odore per il tuo caso specifico, potrebbe essere perché le chiamate di eliminazione vengono eseguite in casi speciali o specifici, non in modo generico. Questo è un design scadente. Un'altra cosa che dovrebbe avere un odore è se new e delete vengono eseguiti mentre non c'è una buona ragione per usare la memoria heap invece di impilarne uno. Il caso ovvio è se un oggetto viene creato e distrutto nella stessa funzione. L'unico caso in cui new / delete è valido allora, è se l'oggetto richiede più memoria di quella consentita dallo stack (e ciò accade!).

Quindi, cerca solo di capire esattamente perché quelle eliminazioni accadono dove si trovano, e se non c'è una buona ragione per loro di essere lì, dovresti rifattorizzare (se possibile).

    
risposta data 13.03.2011 - 20:50
fonte
3

Penso che ci siano validi motivi per usare delete manualmente, specialmente se devi interfacciare librerie che non usano puntatori intelligenti o un set di librerie che usano diversi tipi di puntatori intelligenti. Ma questa dovrebbe essere l'eccezione, non la regola. Se puoi sostituire il delete manuale con un puntatore intelligente, dovresti.

    
risposta data 13.03.2011 - 20:34
fonte
2

Non tutti i software devono utilizzare i puntatori intelligenti. A seconda dell'implementazione del puntatore intelligente e dell'applicazione a portata di mano, possono effettivamente causare più problemi di quanti ne valga.

In alcune situazioni scoprirai che un determinato oggetto possiede un altro oggetto completamente per tutta la sua durata. A quel punto, l'introduzione di un puntatore intelligente per eliminare qualcosa che può essere facilmente eliminato nel distruttore del proprietario potrebbe non essere così utile.

Le app più vecchie che sono state "portate avanti" nell'era del C ++ spesso non hanno nemmeno puntatori intelligenti. Queste cose sono iniziate senza oggetti e sono state martellate fino a quando non avevano oggetti in esse. Queste app hanno spesso un comportamento e una progettazione davvero idiosincratici e i puntatori intelligenti potrebbero semplicemente non farne parte.

I contenitori di puntatori possono anche non essere fatti con puntatori intelligenti perché (ancora, a seconda dell'implementazione del puntatore intelligente disponibile), possono fare cose strane ai pattern di accesso degli oggetti contenuti.

Il motivo principale per cui una determinata applicazione non ha puntatori intelligenti è semplicemente che l'autore originale non aveva familiarità con loro e / o non aveva un'implementazione puntatore intelligente che gli piaceva. Non è alcuna indicazione della qualità del codice circostante.

    
risposta data 13.03.2011 - 18:49
fonte
2

RAII ed eccezioni

With the advent of smart pointers, is it a sign of poor design if I see objects are deleted?

Dipende dal contesto. Diamo un'occhiata a un esempio come questo:

Foo* foo = new Foo;
Bar* bar = new Bar;
func(foo, bar);
delete foo;
delete bar;

* Nota: non utilizzare lo stack in questo esempio è sciocco. Immaginiamo nel mondo reale che Foo e Bar siano dei pimpl, o che siano in definitiva tipi astratti da memorizzare / possedere in una struttura di dati polimorfici, siano creati attraverso una fabbrica, vengano distrutti solo quando l'utente richiede di scaricare la risorsa, ecc.

C'è qualcosa di sbagliato in questo codice? Alcune persone potrebbero pensare "no". Tranne che ci sono tutti i tipi di problemi con questo codice, e non si riferiscono affatto a pratiche o stile - voglio dire che questo codice è buggy . Il problema è questo:

Foo* foo = new Foo; // this can throw
Bar* bar = new Bar; // this can throw
func(foo, bar);     // this might throw
delete foo;
delete bar;

È l'interazione del rilascio manuale delle risorse con la gestione delle eccezioni che rende i distruttori che automatizzano questo spesso una necessità assoluta (vedi RAII). Per fare in modo che il suddetto codice non sia più bacato da percorsi eccezionali senza raggiungere i distruttori per automatizzarlo, potremmo aver bisogno di fare qualcosa del genere:

Foo* foo = new Foo;     // this can throw
try
{
    Bar* bar = new Bar; // this can throw
    try
    {
        func(foo, bar); // this might throw
    }
    catch (...)
    {
        delete bar;
        throw;
    }
    delete bar;
}
catch (...)
{
    delete foo;
    throw;
}
delete foo;

... phew! Questo dovrebbe, se non ho commesso errori (il che è ancora molto possibile), correggere gli errori. Un altro modo è questo:

Foo* foo = new Foo;     // this can throw
Bar* bar = 0;
try
{
    bar = new Bar;      // this can throw
    func(foo, bar);     // this might throw
}
catch (...)
{
    delete foo;
    delete bar;
    throw;
}
delete foo;
delete bar;

... che potrebbe essere un po 'più semplice, ma comunque piuttosto complicato (e immagina quanto sia facile incasinare tutto se siamo tornati e abbiamo apportato delle modifiche in seguito, o se uno dei nostri compagni di squadra l'ha fatto).

Ora consideriamo come possiamo correggere i bug usando i puntatori intelligenti:

unique_ptr<Foo> foo(new Foo);
unique_ptr<Bar> bar(new Bar);
func(foo.get(), bar.get());

Tada! Non so voi, ma sembra molto più facile da leggere, scrivere, gestire. Attraverso i distruttori che rilasciano automaticamente le risorse, finiamo per rendere le nostre vite così, così, molto più semplici.

Exception-Sicurezza

Alcune persone potrebbero pensare che l'esempio sopra sia un po 'discutibile, dal momento che sta correggendo bug in eccezionali percorsi di esecuzione. Almeno per i programmi giocattolo, preoccuparsi del comportamento corretto di fronte a un'eccezione potrebbe essere eccessivo.

Tuttavia, per il codice di produzione, lavoro in un'area che non è molto critica per la missione, ma le persone legano ancora il loro sostentamento professionale (non vite) al software. In questi casi, in questo caso, deve prestare attenzione alla sicurezza delle eccezioni, per consentire al software di ripristinarsi con garbo in caso di eccezioni (e anche scrivere test che simulino percorsi di eccezione lanciando mentre si controllano i rilevatori di fughe e simili). I distruttori che rilasciano automaticamente le risorse e / o ripristinano gli effetti collaterali sono un salvagente qui quando si interagisce con le eccezioni.

Puntatori intelligenti

Detto questo, dobbiamo usare puntatori intelligenti ovunque, di per sé? Qui offrirò una risposta distorta che potrebbe risultare inusuale in quanto si lavora su aree di livello molto basso e ad alte prestazioni.

Non c'è alcun costo per qualcosa come std::unique_ptr a condizione che non ci sia bisogno di un deleter personalizzato. Ma spesso in queste aree in cui lavoro di più, usiamo allocatori personalizzati, richiedendo blocchi da un pool di memoria e restituendoli quando abbiamo finito. Spesso il mio codice non assomiglia a questo:

Foo* foo = new Foo;
...
delete foo;

Invece sembra così usando il nuovo posizionamento:

Foo* foo = new(allocator.allocate()) Foo;
...
foo->~Foo();
allocator.deallocate(foo);

Con questi tipi di casi, può risultare un po 'difficile utilizzare puntatori intelligenti. Quelli standard tendono ad avere un sovraccarico quando sono coinvolti delet personalizzati che vanifica alcuni degli scopi dell'uso di allocatori fissi e tali da migliorare la localizzazione di riferimento (un numero minore di puntatori intelligenti che si inseriscono in una linea della cache). Inoltre, il codice diventa un po 'più complicato.

In questi casi è impossibile persino lanciare i miei puntatori intelligenti senza pagare un costo di runtime, dal momento che dovrebbero essere iniettati con l'allocatore personalizzato che sto usando su singoli oggetti, e questo sarebbe almeno equivalente a raddoppiando la dimensione del puntatore intelligente (lavoro in aree come strutture di dati del grafico dove la rasatura di 8 byte per istanza può fare la differenza tra 1,2 gigabyte di memoria e un gigabyte, ad esempio).

Detto questo, questa non è una scusa per evitare RAII. Quindi quello che faccio è molto attentamente la memoria libera nei miei distruttori (per intere strutture di dati, non per ogni elemento all'interno), per avvolgere questo tipo di codice in oggetti che gestiscono la memoria attraverso il loro costruttore e distruttore, e comunque beneficiare di RAII in quel modo . La gestione delle risorse è ancora automatizzata per coloro che usano le mie classi, devo solo stare attento e implementare i miei distruttori con la gestione della memoria manuale coinvolta e scrivere test per assicurarmi di non dimenticare di liberare memoria in un distruttore.

Questo è un caso eccezionale: per la maggior parte delle persone, direi che è abbastanza saggio usare semplicemente puntatori intelligenti il più possibile per i puntatori che possiedono memoria se la strategia alternativa sta usando il default, lanciando operator new e operator delete . È solo quando sono coinvolti gli allocatori personalizzati che le cose diventano un po 'torbide.

    
risposta data 03.01.2016 - 17:00
fonte
1

Sì. Se è progettato ora.

Se è stato progettato prima del loro avvento, nulla dovrebbe impedirti di sostituire la gestione manuale da parte di smart manager.

La domanda non è:

Should I be using smart managers here ?

Ma:

Which smart manager fits best ?

Se un pezzo di codice moderno viene scritto senza gestori intelligenti, è un'indicazione che l'autore non è aggiornato sulle pratiche C ++ e sarei molto preoccupato per l'aspetto della sicurezza delle eccezioni ....

    
risposta data 13.03.2011 - 19:34
fonte
1

Al momento, c'è una mancanza di puntatori intelligenti nella libreria standard C ++, e Boost non può sempre essere usato, quindi finché non sarà necessario cancellare C ++ 0x.

Rivisitare il vecchio codice che funziona è inutile, indipendentemente dall'evoluzione degli idiomi.

Anche con un buon set di puntatori intelligenti, probabilmente non vorrai usarli se l'intero punto del tuo codice è implementare una struttura dati che non è fornita dalla libreria standard C ++, tra alcuni altri speciali casi, quindi l'eliminazione sarà sempre necessaria in alcuni codici di libreria.

Un punto che vale la pena considerare: i puntatori intelligenti possono avere problemi con i cicli di riferimento. Se i tuoi oggetti possono avere cicli di riferimento, è probabile che l'uso di puntatori intelligenti causi una perdita di memoria (e potenzialmente di risorse). Usare un puntatore intelligente può essere sbagliato, o almeno causare complicazioni senza alcun beneficio pratico, in alcuni rari casi speciali.

    
risposta data 13.03.2011 - 22:19
fonte

Leggi altre domande sui tag