¿Qué sucede en una eliminación doble?

Obj *op = new Obj; Obj *op2 = op; delete op; delete op2; // What happens here? 

¿Qué es lo peor que puede pasar cuando accidentalmente se elimina por duplicado? ¿Importa? ¿El comstackdor lanzará un error?

Causa un comportamiento indefinido. Cualquier cosa puede suceder. En la práctica, es probable que un accidente de tiempo de ejecución sea lo que yo esperaría.

Comportamiento indefinido No hay garantías en absoluto hechas por el estándar. Probablemente su sistema operativo haga algunas garantías, como “no dañará otro proceso”, pero eso no ayuda mucho a su progtwig.

Tu progtwig podría bloquearse. Sus datos podrían estar dañados. El depósito directo de su próximo cheque de pago podría en su lugar sacar 5 millones de dólares de su cuenta.

Es un comportamiento indefinido, por lo que el resultado real variará según el comstackdor y el entorno de tiempo de ejecución.

En la mayoría de los casos, el comstackdor no se dará cuenta. En muchos casos, si no en la mayoría, la biblioteca de administración de memoria de tiempo de ejecución se bloqueará.

Debajo del capó, cualquier administrador de memoria tiene que mantener algunos metadatos sobre cada bloque de datos que asigna, de una manera que le permita buscar los metadatos del puntero que devuelve malloc / new. Por lo general, esto toma la forma de una estructura en desplazamiento fijo antes del bloque asignado. Esta estructura puede contener un “número mágico”, una constante que es poco probable que ocurra por pura casualidad. Si el administrador de memoria ve el número mágico en el lugar esperado, sabe que es muy probable que el puntero proporcionado para liberar / eliminar sea válido. Si no ve el número mágico, o si ve un número diferente que significa “este puntero fue liberado recientemente”, puede ignorar silenciosamente la solicitud gratuita, o puede imprimir un mensaje útil y abortar. Cualquiera de las dos cosas es legal bajo la especificación, y hay argumentos pro / con para cualquiera de los enfoques.

Si el administrador de memoria no mantiene un número mágico en el bloque de metadatos, o no verifica la cordura de los metadatos, entonces puede pasar cualquier cosa. Dependiendo de cómo se implemente el administrador de memoria, el resultado probablemente sea un locking sin un mensaje útil, ya sea inmediatamente en la lógica del administrador de memoria, algo más tarde la próxima vez que el administrador de memoria intente asignar o liberar memoria, o mucho más tarde y más lejos cuando dos partes diferentes del progtwig piensan que poseen el mismo pedazo de memoria.

Vamos a intentarlo. Convierte tu código en un progtwig completo en so.cpp:

 class Obj { public: int x; }; int main( int argc, char* argv[] ) { Obj *op = new Obj; Obj *op2 = op; delete op; delete op2; return 0; } 

Comstackrlo (estoy usando gcc 4.2.1 en OSX 10.6.8, pero YMMV):

 russell@Silverback ~: g++ so.cpp 

Ejecutarlo:

 russell@Silverback ~: ./a.out a.out(1965) malloc: *** error for object 0x100100080: pointer being freed was not allocated *** set a breakpoint in malloc_error_break to debug Abort trap 

Lookie allí, el tiempo de ejecución de gcc en realidad detecta que fue una eliminación doble y es bastante útil antes de que se bloquee.

El comstackdor puede dar una advertencia o algo, especialmente en obvio (como en su ejemplo), pero no es posible detectarlo siempre. (Puede usar algo como valgrind, que en tiempo de ejecución puede detectarlo). En cuanto al comportamiento, puede ser cualquier cosa. Alguna biblioteca segura podría verificar y manejarla bien, pero otros tiempos de ejecución (por velocidad) harán que la suposición de que usted llame sea correcta (lo que no es) y luego se bloquee o empeore. Se permite que el tiempo de ejecución asum que no está eliminando dos veces (incluso si la eliminación doble haría algo malo, por ejemplo, bloqueando su computadora)

Todo el mundo ya te dijo que no deberías hacer esto y que provocará un comportamiento indefinido. Eso es ampliamente conocido, así que profundicemos en esto en un nivel inferior y veamos qué sucede en realidad.

La respuesta universal estándar es que cualquier cosa puede suceder, eso no es del todo cierto. Por ejemplo, la computadora no intentará matarte por hacer esto (a menos que estés progtwigndo AI para un robot) 🙂

La razón por la cual no puede haber una respuesta universal es que, como esto no está definido, puede diferir del comstackdor al comstackdor e incluso a través de diferentes versiones del mismo comstackdor.

Pero esto es lo que “más o menos” sucede en la mayoría de los casos:

delete consta de 2 operaciones principales:

  • llama al destructor si está definido
  • de alguna manera libera la memoria asignada al objeto

Entonces, si su destructor contiene algún código que acceda a cualquier dato de clase que ya fue eliminado, puede segfault O (más probable) usted leerá algunos datos sin sentido. Si estos datos eliminados son punteros, lo más probable es que falle por segmentación, porque intentará acceder a la memoria que contiene algo más, o no le pertenece a usted.

Si su constructor no toca ningún dato o no está presente (no consideremos los destructores virtuales aquí por simplicidad), puede que no sea un motivo de falla en la mayoría de las implementaciones del comstackdor. Sin embargo, llamar a un destructor no es la única operación que va a suceder aquí.

La memoria necesita ser liberada. Cómo se hace depende de la implementación en el comstackdor, pero también puede ejecutar alguna función free , dándole el puntero y el tamaño de su objeto. Llamar free a la memoria que ya fue eliminada puede bloquearse, porque la memoria puede no pertenecerle más. Si le pertenece, es posible que no se bloquee inmediatamente, pero puede sobrescribir la memoria que ya fue asignada para algún objeto diferente de su progtwig.

Eso significa que una o más de sus estructuras de memoria acaban de corromperse y su progtwig probablemente se bloqueará tarde o temprano o podría comportarse de forma increíblemente extraña. Las razones no serán obvias en su depurador y puede pasar semanas averiguando qué diablos acaba de suceder.

Entonces, como han dicho otros, generalmente es una mala idea, pero supongo que ya lo sabes. Pero no te preocupes, es muy probable que un gatito inocente no muera si eliminas un objeto dos veces.

Aquí está el código de ejemplo que está mal pero que también puede funcionar bien (funciona bien con GCC en Linux):

 class a {}; int main() { a *test = new a(); delete test; a *test2 = new a(); delete test; return 0; } 

Si no creo una instancia intermedia de esa clase entre las eliminaciones, las 2 llamadas para liberar en la misma memoria se producen como se esperaba:

 *** Error in `./a.out': double free or corruption (fasttop): 0x000000000111a010 *** 

Para responder a sus preguntas directamente:

¿Qué es lo peor que puede pasar ?

En teoría, tu progtwig causa algo fatal. Incluso puede intentar aleatoriamente borrar tu disco duro en algunos casos extremos. Las posibilidades dependen de cuál es realmente tu progtwig (¿controlador del núcleo? ¿Progtwig de espacio de usuario?).

En la práctica, lo más probable es que se bloquee con segfault. Pero algo peor podría suceder.

¿El comstackdor lanzará un error ?

No debería.

No, no es seguro eliminar el mismo puntero dos veces. Es un comportamiento indefinido según el estándar C ++.

De las preguntas frecuentes de C ++: visite este enlace

¿Es seguro eliminar el mismo puntero dos veces?
¡No! (Suponiendo que no haya recuperado ese puntero de nuevo en el medio).

Por ejemplo, el siguiente es un desastre:

 class Foo { /*...*/ }; void yourCode() { Foo* p = new Foo(); delete p; delete p; // DISASTER! // ... } 

Esa segunda línea p de eliminación podría hacerte algunas cosas realmente malas. Podría, dependiendo de la fase de la luna, corromper su montón, bloquear su progtwig, hacer cambios arbitrarios y extraños en objetos que ya están en el montón, etc. Desafortunadamente, estos síntomas pueden aparecer y desaparecer aleatoriamente. Según la ley de Murphy, será golpeado con todas sus fuerzas en el peor momento posible (cuando el cliente está buscando, cuando una transacción de alto valor está tratando de publicar, etc.). Nota: algunos sistemas de tiempo de ejecución lo protegerán de ciertos casos muy simples de eliminación doble. Dependiendo de los detalles, puede estar bien si se está ejecutando en uno de esos sistemas y si nadie implementa su código en otro sistema que maneje las cosas de manera diferente y si está eliminando algo que no tiene un destructor y si no hace nada significativo entre las dos eliminaciones y si nadie cambia su código para hacer algo significativo entre las dos eliminaciones y si su planificador de subprocesos (sobre el cual probablemente no tenga control) no pasa a intercambiar hilos entre los dos eliminan y si, y si, y si. Así que volvamos a Murphy: dado que puede salir mal, lo hará, y saldrá mal en el peor momento posible. Un no-accidente no prueba la ausencia de un error; simplemente no demuestra la presencia de un error. Confía en mí: borrar dos veces es malo, malo, malo. Solo di no.

Si bien esto no está definido:

 int* a = new int; delete a; delete a; // same as your code 

esto está bien definido:

 int* a = new int; delete a; a = nullptr; // or just NULL or 0 if your compiler doesn't support c++11 delete a; // nothing happens! 

Pensé que debería publicarlo ya que nadie más lo mencionaba.