Le perdite di memoria sono un problema di “comportamento indefinito” in C ++?

Risulta che molte cose dall’aspetto innocente sono un comportamento indefinito in C ++. Ad esempio, una volta che un puntatore non nullo è stato delete , anche la stampa del valore del puntatore è un comportamento non definito .

Ora le perdite di memoria sono decisamente negative. Ma quale situazione di class sono: definita, indefinita o quale altra class di comportamento?

Perdite di memoria.

Non c’è un comportamento indefinito. È perfettamente legale perdere memoria.

Comportamento indefinito: sono le azioni che lo standard non intende definire e lascia all’implementazione in modo tale che sia flessibile eseguire certi tipi di ottimizzazioni senza violare lo standard.

La gestione della memoria è ben definita.
Se si assegna dynamicmente memoria e non la si rilascia. Quindi la memoria rimane di proprietà dell’applicazione da gestire come meglio crede. Il fatto che tu abbia perso tutti i riferimenti a quella porzione di memoria non è né qui né lì.

Naturalmente se continui a perdere, alla fine esaurirai la memoria disponibile e l’applicazione inizierà a generare eccezioni bad_alloc. Ma questo è un altro problema.

Le perdite di memoria sono definitivamente definite in C / C ++.

Se lo faccio:

 int *a = new int[10]; 

seguito da

 a = new int[10]; 

Sto definitivamente perdendo memoria in quanto non c’è modo di accedere al primo array assegnato e questa memoria non viene automaticamente rilasciata poiché GC non è supportato.

Ma le conseguenze di questa perdita sono imprevedibili e variano da applicazione a applicazione e da macchina a macchina per una stessa applicazione. Dire che un’applicazione che si interrompe a causa di perdite su una macchina potrebbe funzionare bene su un’altra macchina con più RAM. Inoltre, per una determinata applicazione su una determinata macchina, l’arresto dovuto alla perdita può comparire in momentjs diversi durante la corsa.

Se si perde memoria, l’esecuzione procede come se nulla accadesse. Questo è un comportamento definito.

In fondo alla traccia, potresti scoprire che una chiamata a malloc fallisce a causa della mancanza di memoria disponibile. Ma questo è un comportamento definito di malloc , e le conseguenze sono anche ben definite: la chiamata malloc restituisce NULL .

Ora questo può causare un programma che non controlla il risultato di malloc per fallire con una violazione di segmentazione. Ma questo comportamento indefinito è (dal POV delle specifiche del linguaggio) a causa del dereferenziamento del programma di un puntatore non valido, non della perdita di memoria precedente o della chiamata malloc fallita.

La mia interpretazione di questa affermazione:

Per un object di un tipo di class con un distruttore non banale, il programma non è obbligato a chiamare esplicitamente il distruttore prima che la memoria che l’object occupa sia riutilizzata o rilasciata; tuttavia, se non esiste una chiamata esplicita al distruttore o se non si utilizza delete-expression (5.3.5) per rilasciare la memoria, il distruttore non deve essere chiamato in modo implicito e qualsiasi programma che dipende dagli effetti collaterali prodotti dal distruttore ha un comportamento indefinito.

è come segue:

Se in qualche modo riesci a liberare la memoria che l’object occupa senza chiamare il distruttore sull’object che occupava la memoria, UB è la conseguenza, se il distruttore non è banale e ha effetti collaterali.

Se il new alloca con malloc , lo storage raw potrebbe essere rilasciato con free() , il distruttore non verrebbe eseguito e l’UB risulterebbe. O se un puntatore viene lanciato su un tipo non correlato ed eliminato, la memoria viene liberata, ma viene eseguito il distruttore sbagliato, UB.

Non è la stessa cosa di un’eliminazione omessa, in cui la memoria sottostante non viene liberata. Omettere l’ delete non è UB.

(Commento sotto “Heads-up: questa risposta è stata spostata qui da Una perdita di memoria causa un comportamento indefinito? ” – probabilmente dovrai leggere quella domanda per ottenere lo sfondo corretto per questa risposta O_o).

Mi sembra che questa parte dello Standard permetta espressamente:

  • disponendo di un pool di memoria personalizzato in cui inserire i new oggetti, quindi rilasciare / riutilizzare l’intera operazione senza spendere tempo chiamando i propri distruttori, a patto che non dipendano dagli effetti collaterali dei distruttori di oggetti .

  • librerie che allocano un po ‘di memoria e non la rilasciano mai, probabilmente perché le loro funzioni / oggetti potrebbero essere usate da distruttori di oggetti statici e registratori on-exit registrati, e non vale la pena di comprare l’intero ordine orchestrato di distruzione o transitorio “phoenix”, come la rinascita ogni volta che si verificano quegli accessi.

Non riesco a capire perché lo Standard scelga di lasciare il comportamento indefinito quando ci sono dipendenze da effetti collaterali – piuttosto che dire semplicemente che quegli effetti collaterali non saranno accaduti e lasciare che il programma abbia un comportamento definito o indefinito come normalmente ci si aspetterebbe quella premessa.

Possiamo ancora considerare che cosa dice lo standard è un comportamento indefinito. La parte cruciale è:

“dipende dagli effetti collaterali prodotti dal distruttore ha un comportamento indefinito.”

Lo standard §1.9 / 12 definisce esplicitamente gli effetti collaterali come segue (i corsivi in ​​basso sono gli standard, che indicano l’introduzione di una definizione formale):

Accedere a un object designato da un glValue volatile (3.10), modificare un object, chiamare una funzione I / O di libreria o chiamare una funzione che fa una di queste operazioni sono tutti effetti collaterali , che sono cambiamenti nello stato dell’ambiente di esecuzione.

Nel tuo programma, non c’è dipendenza quindi nessun comportamento indefinito.

Un esempio di dipendenza che probabilmente corrisponde allo scenario di §3.8 p4, dove non è evidente la necessità o la causa di un comportamento indefinito, è:

 struct X { ~X() { std::cout << "bye!\n"; } }; int main() { new X(); } 

Un problema che le persone discutono è se l'object X sopra sarebbe considerato released ai fini di 3.8 p4, dato che è probabilmente rilasciato solo al sistema operativo dopo la chiusura del programma - non è chiaro dalla lettura dello standard se quella fase della "vita" di un processo è nel campo di applicazione dei requisiti comportamentali dello Standard (la mia ricerca rapida dello Standard non ha chiarito questo aspetto). Personalmente mi azzarderei ad applicare 3.8p4 qui, in parte perché fintanto che è abbastanza ambiguo da argomentare uno scrittore di compilatori può sentirsi autorizzato a consentire un comportamento indefinito in questo scenario, ma anche se il codice precedente non costituisce il rilascio lo scenario è facilmente emendamento modificato ...

 int main() { X* p = new X(); *(char*)p = 'x'; // token memory reuse... } 

Ad ogni modo, comunque il main implementato, il distruttore di cui sopra ha un effetto collaterale - per "chiamare una funzione I / O della libreria"; inoltre, il comportamento osservabile del programma probabilmente "dipende" da esso, nel senso che i buffer che sarebbero stati influenzati dal distruttore nel caso in cui esso fosse stato eseguito, vengono svuotati durante la terminazione. Ma "dipende dagli effetti collaterali" intendeva solo alludere a situazioni in cui il programma avrebbe chiaramente un comportamento indefinito se il distruttore non fosse stato eseguito? Errerei dal lato del primo, in particolare perché quest'ultimo caso non avrebbe bisogno di un paragrafo dedicato nello standard per documentare che il comportamento non è definito. Ecco un esempio con un comportamento chiaramente non definito:

 int* p_; struct X { ~X() { if (b_) p_ = 0; else delete p_; } bool b_; }; X x{true}; int main() { p_ = new int(); delete p_; // p_ now holds freed pointer new (&x){false}; // reuse x without calling destructor } 

Quando viene chiamato il distruttore di x durante la terminazione, b_ sarà false e ~X() delete p_ quindi delete p_ per un puntatore già liberato, creando un comportamento non definito. Se x.~X(); era stato chiamato prima del riutilizzo, p_ sarebbe stato impostato a 0 e la cancellazione sarebbe stata sicura. In questo senso, si può affermare che il comportamento corretto del programma dipende dal distruttore e il comportamento è chiaramente indefinito, ma abbiamo appena creato un programma che corrisponda al comportamento descritto 3.8p4 a sé stante, piuttosto che avere il comportamento come conseguenza di 3.8p4 ...?

Scenari più sofisticati con problemi - troppo lunghi per fornire il codice - potrebbero includere, ad esempio, una strana libreria C ++ con contatori di riferimento all'interno di oggetti di stream di file che dovevano colpire 0 per triggersre alcune elaborazioni come l'I / O di svuotamento o l'unione di thread in background ecc. dove l'incapacità di fare queste cose rischiava non solo di non riuscire a eseguire l'output richiesto esplicitamente dal distruttore, ma anche di non emettere altro output bufferizzato dallo stream, o su alcuni sistemi operativi con un filesystem transazionale potrebbe causare un rollback di I / O precedenti - tali questioni potrebbero cambiare il comportamento del programma osservabile o addirittura lasciare il programma bloccato.

Nota: non è necessario dimostrare che esiste un codice effettivo che si comporta in modo strano su qualsiasi compilatore / sistema esistente; lo Standard si riserva chiaramente il diritto per i compilatori di avere un comportamento indefinito ... questo è tutto ciò che conta. Questo non è qualcosa su cui puoi ragionare e scegli di ignorare lo Standard - potrebbe essere che C ++ 14 o qualche altra revisione modifichi questa clausola, ma finché è lì allora se c'è forse anche una certa "dipendenza" dagli effetti collaterali allora c'è il potenziale per un comportamento indefinito (che di per sé è permesso essere definito da un particolare compilatore / implementazione, quindi non significa automaticamente che ogni compilatore è obbligato a fare qualcosa di bizzarro).

Le specifiche del linguaggio non dicono nulla sulle “perdite di memoria”. Dal punto di vista della lingua, quando crei un object nella memoria dynamic, stai facendo proprio questo: stai creando un object anonimo con durata illimitata / durata di archiviazione. “Illimitato” in questo caso significa che l’object può terminare la sua durata di vita / archiviazione solo quando viene esplicitamente deallocato, ma altrimenti continua a vivere per sempre (a condizione che il programma venga eseguito).

Ora, di solito consideriamo che un object allocato dynamicmente diventi una “perdita di memoria” al momento dell’esecuzione del programma quando tutti i riferimenti (generici “riferimenti”, come i puntatori) a quell’object vengono persi al punto di essere irrecuperabili. Nota che anche per un essere umano la nozione di “tutti i riferimenti sono persi” non è definita con precisione. Cosa succede se abbiamo un riferimento ad una parte dell’object, che può essere teoricamente “ricalcolato” a un riferimento all’intero object? È una perdita di memoria o no? Cosa succede se non abbiamo alcun riferimento all’object qualunque, ma in qualche modo possiamo calcolare tale riferimento utilizzando alcune altre informazioni disponibili per il programma (come una precisa sequenza di allocazioni)?

Le specifiche del linguaggio non si preoccupano di problemi del genere. Qualunque cosa tu consideri un’apparizione di “perdita di memoria” nel tuo programma, dal punto di vista della lingua è un non-evento. Dal punto di vista della lingua, un object “trapelato” assegnato dynamicmente continua a vivere felicemente fino alla fine del programma. Questo è l’unico punto di preoccupazione rimanente: cosa succede quando il programma finisce e parte della memoria dynamic è ancora allocata?

Se ricordo bene, il linguaggio non specifica cosa succede alla memoria dynamic a cui è ancora assegnato il momento di terminazione del programma. Non verranno effettuati tentativi per distruggere / deallocare automaticamente gli oggetti creati nella memoria dynamic. Ma non c’è un comportamento formale indefinito in casi come quello.

L’onere della prova è su coloro che pensano che una perdita di memoria potrebbe essere C ++ UB.

Naturalmente non sono state presentate prove.

In breve, per chiunque nutra dubbi, questa domanda non può mai essere risolta in modo chiaro, tranne che minacciò in modo molto credibile il comitato con la musica ad alto volume di Justin Bieber, in modo tale che aggiungano una dichiarazione C ++ 14 che chiarisca che non è UB.


In questione è C ++ 11 §3.8 / 4:

Per un object di un tipo di class con un distruttore non banale, il programma non è obbligato a chiamare esplicitamente il distruttore prima che la memoria che l’object occupa sia riutilizzata o rilasciata; tuttavia, se non esiste una chiamata esplicita al distruttore o se non si utilizza delete-expression (5.3.5) per rilasciare la memoria, il distruttore non deve essere chiamato in modo implicito e qualsiasi programma che dipende dagli effetti collaterali prodotti dal distruttore ha un comportamento indefinito.

Questo passaggio aveva la stessa identica formulazione in C ++ 98 e C ++ 03. Cosa significa?

  • il programma non è obbligato a chiamare esplicitamente il distruttore prima che la memoria occupata dall’object venga riutilizzata o rilasciata

    – significa che si può afferrare la memoria di una variabile e riutilizzare quella memoria, senza prima distruggere l’object esistente.

  • se non c’è una chiamata esplicita al distruttore o se non si usa delete-expression (5.3.5) per rilasciare la memoria, il distruttore non deve essere chiamato implicitamente

    – significa che se uno non distrugge l’object esistente prima del riutilizzo della memoria, se l’object è tale che viene chiamato automaticamente il suo distruttore (ad es. Una variabile automatica locale) allora il programma ha un comportamento non definito, perché quel distruttore opererebbe quindi su un no object esistente più lungo.

  • e qualsiasi programma che dipende dagli effetti collaterali prodotti dal distruttore ha un comportamento indefinito

    – non può significare letteralmente quello che dice, perché un programma dipende sempre da qualsiasi effetto collaterale, dalla definizione di effetto collaterale. O in altre parole, non c’è modo per il programma di non dipendere dagli effetti collaterali, perché quindi non sarebbero effetti collaterali.

Molto probabilmente ciò che era inteso non era quello che finalmente si faceva strada nel C ++ 98, così che ciò che abbiamo a portata di mano è un difetto .

Dal contesto si può intuire che se un programma si basa sulla distruzione automatica di un object di tipo T staticamente noto, in cui la memoria è stata riutilizzata per creare un object o oggetti che non è un object T , allora si tratta di comportamento indefinito.


Coloro che hanno seguito il commentario potrebbero notare che la precedente spiegazione della parola “deve” non è il significato che ho assunto in precedenza. Come lo vedo ora, il “deve” non è un requisito per l’implementazione, cosa è permesso fare. È un requisito del programma, ciò che il codice è autorizzato a fare.

Quindi, questo è formalmente UB:

 auto main() -> int { string s( 666, '#' ); new( &s ) string( 42, '-' ); // <- Storage reuse. cout << s << endl; // <- Formal UB, because original destructor implicitly invoked. } 

Ma va bene con un'interpretazione letterale:

 auto main() -> int { string s( 666, '#' ); s.~string(); new( &s ) string( 42, '-' ); // <- Storage reuse. cout << s << endl; // OK, because of the explicit destruction of the original object. } 

Un problema principale è che con un'interpretazione letterale del paragrafo dello standard sopra sarebbe ancora formalmente OK se il posizionamento nuovo creava un object di un tipo diverso lì, proprio a causa della distruzione esplicita dell'originale. Ma in questo caso non sarebbe molto male nella pratica. Forse questo è coperto da qualche altro paragrafo nello standard, quindi è anche formalmente UB.

E anche questo è OK, usando il new posizionamento da :

 auto main() -> int { char* storage = new char[sizeof( string )]; new( storage ) string( 666, '#' ); string const& s = *( new( storage ) string( 42, '-' ) // <- Storage reuse. ); cout << s << endl; // OK, because no implicit call of original object's destructor. } 

Come la vedo io - ora.

Il suo comportamento definito in modo definitivo .

Si consideri un caso in cui il server è in esecuzione e continua a allocare memoria heap e nessuna memoria viene rilasciata anche se non è utilizzata. Quindi il risultato finale sarebbe che alla fine il server esaurirà la memoria e sicuramente si verificherà un crash.

Aggiungendo a tutte le altre risposte, un approccio completamente diverso. Guardando l’allocazione di memoria in § 5.3.4-18 possiamo vedere:

Se qualsiasi parte dell’inizializzazione dell’object descritta sopra 76 termina lanciando un’eccezione e una funzione di deallocazione appropriata può essere trovata, la funzione deallocazione viene chiamata per liberare la memoria in cui è stato costruito l’object, dopodiché l’eccezione continua a propagarsi nel contesto della nuova espressione. Se non è ansible trovare alcuna funzione di deallocazione di corrispondenza non ambigua, la propagazione dell’eccezione non causa la liberazione della memoria dell’object. [Nota: questo è appropriato quando la funzione di allocazione chiamata non alloca memoria; in caso contrario, è probabile che si verifichi una perdita di memoria. -End note]

Potrebbe causare UB qui, sarebbe menzionato, quindi è “solo una perdita di memoria”.

In posti come §20.6.4-10, viene menzionato un ansible netturbino e rilevatore di perdite. Un sacco di pensiero è stato messo nel concetto di indicatori di derivazione sicura et.al. essere in grado di usare C ++ con un garbage collector (C.2.10 “Supporto minimo per le regioni raccolte da rifiuti”).

Quindi, se fosse UB a perdere l’ultimo puntatore su qualche object, tutto lo sforzo non avrebbe senso.

Per quanto riguarda il “quando il distruttore ha effetti collaterali che non lo usano mai UB” direi che questo è sbagliato, altrimenti le strutture come std::quick_exit() sarebbero intrinsecamente anche per UB.

Se lo space shuttle deve decollare entro due minuti, e ho una scelta tra metterlo in codice con perdite di memoria e codice che ha un comportamento indefinito, sto inserendo il codice che perde memoria.

Ma la maggior parte di noi di solito non si trova in una situazione del genere, e se lo siamo, probabilmente è un fallimento più avanti. Forse mi sbaglio, ma sto leggendo questa domanda come, “Quale peccato mi porterà all’inferno più velocemente?”

Probabilmente il comportamento indefinito, ma in realtà entrambi.

definito, dal momento che una perdita di memoria ti sta dimenticando di ripulire dopo te stesso.

naturalmente, una perdita di memoria può probabilmente causare un comportamento indefinito in seguito.

Risposta immediata: lo standard non definisce cosa succede quando si perde la memoria, quindi è “indefinito”. È implicitamente indefinito però, che è meno interessante delle cose esplicitamente indefinite nello standard.

Questo ovviamente non può essere un comportamento indefinito. Semplicemente perché UB deve accadere ad un certo punto nel tempo, e dimenticare di rilasciare memoria o chiamare un distruttore non accade in qualsiasi momento. Quello che succede è solo che il programma termina senza aver mai rilasciato memoria o chiamato il distruttore; questo non rende il comportamento del programma, o della sua conclusione, indefinito in alcun modo.

Detto questo, a mio parere lo standard si contraddice in questo passo. Da una parte garantisce che il distruttore non venga chiamato in questo scenario e, d’altra parte, dice che se il programma dipende dagli effetti collaterali prodotti dal distruttore, ha un comportamento indefinito. Supponiamo che le chiamate del distruttore exit , quindi nessun programma che fa qualcosa può fingere di essere indipendente da quello, perché l’effetto collaterale di chiamare il distruttore gli impedirebbe di fare ciò che altrimenti farebbe; ma il testo assicura anche che il distruttore non sarà chiamato in modo che il programma possa continuare a fare le sue cose indisturbate. Penso che l’unico modo ragionevole per leggere la fine di questo passaggio sia che se il comportamento corretto del programma richiede che il distruttore venga chiamato, il comportamento non è in effetti definito; questa è quindi un’osservazione superflua, dato che è stato appena stabilito che il distruttore non sarà chiamato.

Undefined behavior means, what will happen has not been defined or is unknown. The behavior of memory leaks is definitly known in C/C++ to eat away at available memory. The resulting problems, however, can not always be defined and vary as described by gameover.