È sicuro eliminare un puntatore vuoto?


96

Supponiamo di avere il seguente codice:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

È sicuro? O deve ptressere eseguito il cast char*prima dell'eliminazione?


Perché stai gestendo la memoria da solo? Quale struttura dati stai creando? La necessità di eseguire una gestione esplicita della memoria è piuttosto rara in C ++; di solito dovresti usare classi che lo gestiscono per te dall'STL (o da Boost in un pizzico).
Brian

6
Solo per le persone che leggono, utilizzo le variabili void * come parametri per i miei thread in win c ++ (vedi _beginthreadex). Di solito puntano acutamente alle classi.
ryansstack

3
In questo caso è un wrapper generico per new / delete, che potrebbe contenere statistiche di tracciamento dell'allocazione o un pool di memoria ottimizzato. In altri casi, ho visto puntatori a oggetti memorizzati in modo errato come variabili membro void * e cancellati in modo errato nel distruttore senza eseguire il cast al tipo di oggetto appropriato. Quindi ero curioso della sicurezza / insidie.
An̲̳̳drew

1
Per un wrapper generico per new / delete è possibile sovraccaricare gli operatori new / delete. A seconda dell'ambiente che usi, probabilmente otterrai degli hook nella gestione della memoria per tenere traccia delle allocazioni. Se ti ritrovi in ​​una situazione in cui non sai cosa stai eliminando, prendilo come un forte suggerimento che il tuo design non è ottimale e necessita di refactoring.
Hans

38
Penso che ci siano troppe domande sulla domanda invece di rispondere. (Non solo qui, ma in tutto SO)
Petruza

Risposte:


24

Dipende dalla "sicurezza". Di solito funzionerà perché le informazioni vengono memorizzate insieme al puntatore sull'allocazione stessa, quindi il deallocatore può riportarle nel posto giusto. In questo senso è "sicuro" fintanto che l'allocatore utilizza tag di confine interni. (Molti lo fanno.)

Tuttavia, come accennato in altre risposte, l'eliminazione di un puntatore void non chiamerà i distruttori, il che può essere un problema. In questo senso, non è "sicuro".

Non c'è una buona ragione per fare quello che stai facendo nel modo in cui lo stai facendo. Se si desidera scrivere le proprie funzioni di deallocazione, è possibile utilizzare i modelli di funzione per generare funzioni con il tipo corretto. Un buon motivo per farlo è generare allocatori di pool, che possono essere estremamente efficienti per tipi specifici.

Come accennato in altre risposte, questo è un comportamento indefinito in C ++. In generale è bene evitare comportamenti indefiniti, sebbene l'argomento stesso sia complesso e pieno di opinioni contrastanti.


9
In che modo questa è una risposta accettata? Non ha alcun senso "eliminare un puntatore vuoto" - la sicurezza è un punto controverso.
Kerrek SB

6
"Non c'è nessuna buona ragione per fare quello che stai facendo nel modo in cui lo stai facendo." Questa è la tua opinione, non un fatto.
rxantos

8
@rxantos Fornisci un contro esempio in cui fare ciò che l'autore della domanda vuole fare è una buona idea in C ++.
Christopher

Penso che questa risposta è in realtà lo più ragionevole, ma penso anche che qualsiasi risposta a questa domanda esigenze almeno menzionare che questo è un comportamento indefinito.
Kyle Strand

@Christopher Prova a scrivere un unico sistema di garbage collector che non sia specifico per il tipo ma semplicemente funzioni. Il fatto che sizeof(T*) == sizeof(U*)per tutti T,Usuggerisce che dovrebbe essere possibile avere 1 void *implementazione di garbage collector basata su modelli non . Ma poi, quando il gc deve effettivamente eliminare / liberare un puntatore, sorge esattamente questa domanda. Per farlo funzionare, o hai bisogno di wrapper distruttori di funzioni lambda (urgh) o avresti bisogno di una sorta di tipo dinamico "tipo come dati" che consenta di andare avanti e indietro tra un tipo e qualcosa di memorizzabile.
BitTickler

147

L'eliminazione tramite un puntatore void non è definita dallo standard C ++ - vedere la sezione 5.3.5 / 3:

Nella prima alternativa (elimina oggetto), se il tipo statico dell'operando è diverso dal suo tipo dinamico, il tipo statico deve essere una classe base del tipo dinamico dell'operando e il tipo statico deve avere un distruttore virtuale o il comportamento è indefinito . Nella seconda alternativa (elimina array) se il tipo dinamico dell'oggetto da eliminare è diverso dal suo tipo statico, il comportamento è indefinito.

E la sua nota a piè di pagina:

Ciò implica che un oggetto non può essere cancellato utilizzando un puntatore di tipo void * perché non ci sono oggetti di tipo void

.


6
Sei sicuro di aver centrato la citazione giusta? Penso che la nota a piè di pagina si riferisca a questo testo: "Nella prima alternativa (elimina oggetto), se il tipo statico dell'operando è diverso dal suo tipo dinamico, il tipo statico deve essere una classe base del tipo dinamico dell'operando e il tipo statico il tipo deve avere un distruttore virtuale o il comportamento non è definito. Nella seconda alternativa (elimina array) se il tipo dinamico dell'oggetto da eliminare è diverso dal suo tipo statico, il comportamento è indefinito. " :)
Johannes Schaub - litb

2
Hai ragione - ho aggiornato la risposta. Non credo che neghi il punto fondamentale però?

No certo che no. Dice ancora che è UB. Ancora di più, ora afferma in modo normativo che l'eliminazione di void * è UB :)
Johannes Schaub - litb

Riempire la memoria degli indirizzi puntati di un puntatore vuoto con NULLfare qualche differenza per la gestione della memoria dell'applicazione?
artu-hnrq

25

Non è una buona idea e non è qualcosa che faresti in C ++. Stai perdendo le informazioni sul tipo senza motivo.

Il tuo distruttore non verrà chiamato sugli oggetti nel tuo array che stai eliminando quando lo chiami per tipi non primitivi.

Dovresti invece sovrascrivere new / delete.

Eliminare il vuoto * probabilmente libererà la memoria correttamente per caso, ma è sbagliato perché i risultati non sono definiti.

Se per qualche motivo a me sconosciuto hai bisogno di memorizzare il tuo puntatore in un vuoto * quindi liberalo, dovresti usare malloc e free.


5
Hai ragione sul fatto che il distruttore non venga chiamato, ma hai torto sul fatto che la dimensione sia sconosciuta. Se si dà eliminare un puntatore che avete ottenuto da nuovo, fa infatti conosce la dimensione della cosa viene cancellato, il tutto a parte il tipo. Il modo in cui funziona non è specificato dallo standard C ++, ma ho visto implementazioni in cui la dimensione viene memorizzata immediatamente prima dei dati a cui punta il puntatore restituito da "new".
KeyserSoze

Rimossa la parte relativa alla dimensione, sebbene lo standard C ++ dica che non è definita. So che malloc / free funzionerebbe anche per i puntatori void *.
Brian R. Bondy

Non supponi di avere un collegamento web alla sezione pertinente dello standard? So che le poche implementazioni di new / delete che ho visto funzionano sicuramente correttamente senza la conoscenza del tipo, ma ammetto di non aver esaminato ciò che specifica lo standard. IIRC C ++ originariamente richiedeva un conteggio degli elementi dell'array durante l'eliminazione degli array, ma non lo fa più con le versioni più recenti.
KeyserSoze

Si prega di vedere la risposta di @Neil Butterworth. La sua risposta dovrebbe essere quella accettata secondo me.
Brian R. Bondy

2
@keysersoze: Generalmente non sono d'accordo con la tua affermazione. Solo perché alcune implementazioni hanno archiviato le dimensioni prima della memoria allocata non significa che sia una regola.

13

Eliminare un puntatore void è pericoloso perché i distruttori non verranno chiamati sul valore a cui punta effettivamente. Ciò può causare perdite di memoria / risorse nell'applicazione.


1
char non ha un costruttore / distruttore.
rxantos

9

La domanda non ha senso. La tua confusione potrebbe essere in parte dovuta al linguaggio sciatto con cui le persone usano spessodelete :

Si usa deleteper distruggere un oggetto che è stato allocato dinamicamente. In questo modo, formerai un'espressione di cancellazione con un puntatore a quell'oggetto . Non "cancelli mai un puntatore". Quello che fai veramente è "eliminare un oggetto identificato dal suo indirizzo".

Ora vediamo perché la domanda non ha senso: un puntatore vuoto non è "l'indirizzo di un oggetto". È solo un indirizzo, senza semantica. Essa può provenire da l'indirizzo di un oggetto reale, ma che l'informazione è persa, perché è stato codificato nel tipo del puntatore originale. L'unico modo per ripristinare un puntatore a un oggetto è restituire il puntatore a un oggetto (il che richiede all'autore di sapere cosa significa il puntatore). voidesso stesso è un tipo incompleto e quindi mai il tipo di un oggetto, e un puntatore void non può mai essere usato per identificare un oggetto. (Gli oggetti sono identificati congiuntamente dal tipo e dal loro indirizzo.)


Certo, la domanda non ha molto senso senza alcun contesto circostante. Alcuni compilatori C ++ compileranno ancora felicemente un codice così assurdo (se si sentono utili, potrebbero abbaiare un avvertimento al riguardo). Quindi, la domanda è stata posta per valutare i rischi noti dell'esecuzione di codice legacy che contiene questa operazione sconsiderata: andrà in crash? perdere parte o tutta la memoria dell'array di caratteri? qualcos'altro che è specifico della piattaforma?
Andrew

Grazie per la premurosa risposta. Upvoting!
Andrew

1
@Andrew: temo che lo standard sia abbastanza chiaro su questo: "Il valore dell'operando di deletepuò essere un valore di puntatore nullo, un puntatore a un oggetto non array creato da una nuova espressione precedente o un puntatore a un oggetto secondario che rappresenta una classe base di tale oggetto. In caso contrario, il comportamento non è definito. " Quindi, se un compilatore accetta il codice senza una diagnostica, non è altro che un bug nel compilatore ...
Kerrek SB

1
@ KerrekSB - Re non è altro che un bug nel compilatore - Non sono d'accordo. Lo standard dice che il comportamento non è definito. Ciò significa che il compilatore / implementazione può fare qualsiasi cosa ed essere comunque conforme allo standard. Se la risposta del compilatore è che non puoi eliminare un puntatore void *, va bene. Se la risposta del compilatore è di cancellare il disco rigido, va bene anche questo. OTOH, se la risposta del compilatore è di non generare alcuna diagnostica ma invece di generare codice che libera la memoria associata a quel puntatore, anche questo va bene. Questo è un modo semplice per affrontare questa forma di UB.
David Hammen,

Solo per aggiungere: non sto perdonando l'uso di delete void_pointer. È un comportamento indefinito. I programmatori non dovrebbero mai invocare un comportamento indefinito, anche se la risposta sembra fare ciò che il programmatore voleva che fosse fatto.
David Hammen,

6

Se davvero deve fare questo, perché non tagliare fuori l'uomo medio (le newe deleteoperatori) e chiamare il globale operator newe operator deletedirettamente? (Ovviamente, se stai cercando di strumentare gli operatori newe delete, in realtà dovresti reimplementare operator newe operator delete.)

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Nota che a differenza di malloc(), operator newgenera std::bad_allocin caso di errore (o chiama il new_handlerse uno è registrato).


Questo è corretto, poiché char non ha un costruttore / distruttore.
rxantos

5

Perché char non ha una logica distruttore speciale. QUESTO non funzionerà.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

L'autore non verrà chiamato.


5

Se vuoi usare void *, perché non usi solo malloc / free? new / delete è più della semplice gestione della memoria. Fondamentalmente, new / delete chiama un costruttore / distruttore e ci sono più cose in corso. Se usi solo i tipi incorporati (come char *) e li elimini tramite void *, funzionerebbe ma comunque non è raccomandato. La linea di fondo è usare malloc / free se vuoi usare void *. Altrimenti, puoi utilizzare le funzioni del modello per tua comodità.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}

Non ho scritto il codice nell'esempio: mi sono imbattuto in questo modello utilizzato in un paio di punti, mescolando curiosamente la gestione della memoria C / C ++ e mi chiedevo quali fossero i pericoli specifici.
An̲̳̳drew

Scrivere C / C ++ è una ricetta per il fallimento. Chiunque l'abbia scritto avrebbe dovuto scrivere l'uno o l'altro.
David Thornley,

2
@David Questo è C ++, non C / C ++. C non ha modelli, né usa new e delete.
rxantos

5

Molte persone hanno già commentato dicendo che no, non è sicuro eliminare un puntatore vuoto. Sono d'accordo con questo, ma volevo anche aggiungere che se stai lavorando con puntatori void per allocare array contigui o qualcosa di simile, puoi farlo in newmodo da poterlo usare in deletesicurezza (con, ahem , un po 'di lavoro extra). Questo viene fatto assegnando un puntatore vuoto alla regione di memoria (chiamata "arena") e quindi fornendo il puntatore all'arena a new. Vedere questa sezione nelle domande frequenti su C ++ . Questo è un approccio comune all'implementazione dei pool di memoria in C ++.


0

Non c'è quasi un motivo per farlo.

Prima di tutto, se non conosci il tipo di dati, e tutto quello che sai è che lo è void*, allora dovresti davvero trattare quei dati come un blob senza tipo di dati binari ( unsigned char*) e usare malloc/ freeper gestirli . Questo è necessario a volte per cose come i dati delle forme d'onda e simili, dove è necessario passarevoid* puntatori alle API C. Va bene.

Se fai conoscere il tipo di dati (cioè ha un ctor / dtor), ma per qualche ragione si è conclusa con un void*puntatore (per qualsiasi motivo avete) allora si dovrebbe davvero gettato di nuovo al tipo di sapere che a essere , e chiamalo delete.


0

Ho usato void *, (aka tipi sconosciuti) nel mio framework per la riflessione sul codice e altre imprese di ambiguità, e finora non ho avuto problemi (perdita di memoria, violazioni di accesso, ecc.) Da nessun compilatore. Solo avvertimenti dovuti al fatto che l'operazione non è standard.

Ha perfettamente senso eliminare uno sconosciuto (void *). Assicurati solo che il puntatore segua queste linee guida, altrimenti potrebbe smettere di avere senso:

1) Il puntatore sconosciuto non deve puntare a un tipo che ha un decostruttore banale, quindi quando lanciato come puntatore sconosciuto non dovrebbe MAI ESSERE ELIMINATO. Elimina il puntatore sconosciuto solo DOPO averlo ripristinato nel tipo ORIGINALE.

2) L'istanza viene referenziata come puntatore sconosciuto nella memoria associata allo stack o all'heap? Se il puntatore sconosciuto fa riferimento a un'istanza sullo stack, non dovrebbe MAI ESSERE ELIMINATO!

3) Sei sicuro al 100% che il puntatore sconosciuto sia una regione di memoria valida? No, allora non dovrebbe MAI ESSERE DELTATO!

In tutto, c'è pochissimo lavoro diretto che può essere fatto usando un tipo di puntatore sconosciuto (void *). Tuttavia, indirettamente, il vuoto * è una grande risorsa per gli sviluppatori C ++ su cui fare affidamento quando è richiesta l'ambiguità dei dati.


0

Se vuoi solo un buffer, usa malloc / free. Se devi usare new / delete, considera una banale classe wrapper:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;

0

Per il caso particolare del char.

char è un tipo intrinseco che non dispone di un distruttore speciale. Quindi l'argomento delle fughe di notizie è discutibile.

sizeof (char) è di solito uno quindi non ci sono nemmeno argomenti di allineamento. Nel caso di piattaforme rare in cui la dimensione di (char) non è una, allocano la memoria sufficientemente allineata per il loro carattere. Quindi anche l'argomento dell'allineamento è discutibile.

malloc / free sarebbe più veloce in questo caso. Ma perdi std :: bad_alloc e devi controllare il risultato di malloc. Chiamare gli operatori globali new e delete potrebbe essere migliore in quanto aggira l'intermediario.


1
" sizeof (char) è solitamente uno " sizeof (char) è SEMPRE uno
curioso

Fino a poco tempo fa (2019) la gente pensa che in newrealtà sia definito da lanciare. Questo non è vero. Dipende dal compilatore e dal commutatore del compilatore. Vedi ad esempio gli /GX[-] enable C++ EH (same as /EHsc)switch MSVC2019 . Anche sui sistemi embedded molti scelgono di non pagare le tasse sulle prestazioni per le eccezioni C ++. Quindi la frase che inizia con "But you forfeit std :: bad_alloc ..." è discutibile.
BitTickler
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.