¿Es una buena práctica anotar un puntero después de eliminarlo?

Comenzaré diciendo, use punteros inteligentes y nunca tendrá que preocuparse por esto.

¿Cuáles son los problemas con el siguiente código?

Foo * p = new Foo; // (use p) delete p; p = NULL; 

Esto fue provocado por una respuesta y comentarios a otra pregunta. Un comentario de Neil Butterworth generó algunos votos ascendentes:

Establecer punteros a NULL después de eliminar no es una buena práctica universal en C ++. Hay ocasiones en las que es bueno hacerlo, y momentos en los que no tiene sentido y puede ocultar errores.

Hay muchas circunstancias en las que no ayudaría. Pero en mi experiencia, no puede doler. Alguien me ilumine.

Establecer un puntero a 0 (que es “nulo” en C ++ estándar, la definición NULL de C es algo diferente) evita lockings en eliminaciones dobles.

Considera lo siguiente:

 Foo* foo = 0; // Sets the pointer to 0 (C++ NULL) delete foo; // Won't do anything 

Mientras:

 Foo* foo = new Foo(); delete foo; // Deletes the object delete foo; // Undefined behavior 

En otras palabras, si no establece los punteros borrados en 0, se meterá en problemas si está realizando eliminaciones dobles. Un argumento en contra de establecer punteros a 0 después de eliminar sería que al hacerlo, solo las máscaras eliminan errores y los dejan sin manejar.

Lo mejor es no tener bugs de eliminación doble, obviamente, pero dependiendo de la semántica de propiedad y los ciclos de vida de los objetos, esto puede ser difícil de lograr en la práctica. Prefiero un error de eliminación doble enmascarado sobre UB.

Finalmente, una nota al respecto sobre la administración de la asignación de objetos, sugiero que eche un vistazo a std::unique_ptr para la propiedad estricta / singular, std::shared_ptr para la propiedad compartida u otra implementación de puntero inteligente, según sus necesidades.

Establecer punteros a NULL después de que haya borrado lo que señalaba ciertamente no puede doler, pero a menudo es una especie de curita sobre un problema más fundamental: ¿por qué está usando un puntero en primer lugar? Puedo ver dos razones típicas:

  • Simplemente quería algo asignado en el montón. En cuyo caso, envolverlo en un objeto RAII hubiera sido mucho más seguro y limpio. Finalice el scope del objeto RAII cuando ya no necesite el objeto. Así es como std::vector funciona, y resuelve el problema de dejar accidentalmente punteros para desasignar la memoria. No hay punteros.
  • O quizás quisiste una semántica compleja de propiedad compartida. El puntero devuelto desde el new podría no ser el mismo al que se llama a la delete . Múltiples objetos pueden haber usado el objeto simultáneamente mientras tanto. En ese caso, un puntero compartido o algo similar hubiera sido preferible.

Mi regla de oro es que si deja punteros en el código de usuario, lo está haciendo mal. El puntero no debería estar allí para señalar basura en primer lugar. ¿Por qué no hay un objeto que asum la responsabilidad de garantizar su validez? ¿Por qué no termina su scope cuando lo hace el objeto apuntado?

Tengo una mejor práctica aún mejor: cuando sea posible, ¡termine el scope de la variable!

 { Foo* pFoo = new Foo; // use pFoo delete pFoo; } 

Siempre configuro un puntero a NULL (ahora nullptr ) después de eliminar los objetos a los que apunta.

  1. Puede ayudar a capturar muchas referencias a la memoria liberada (suponiendo que su plataforma falla en un deref de un puntero nulo).

  2. No captará todas las referencias a memoria libre si, por ejemplo, tiene copias del puntero. Pero algunos son mejores que ninguno.

  3. Va a enmascarar una eliminación doble, pero creo que son mucho menos comunes que los accesos a la memoria ya liberada.

  4. En muchos casos, el comstackdor lo va a optimizar. Entonces el argumento de que es innecesario no me convence.

  5. Si ya está usando RAII, entonces no hay muchas delete en su código, por lo que el argumento de que la asignación adicional genera confusión no me convence.

  6. A menudo, cuando se depura, es conveniente ver el valor nulo en lugar de un puntero obsoleto.

  7. Si esto todavía te molesta, utiliza un puntero inteligente o una referencia en su lugar.

También configuré otros tipos de identificadores de recursos para el valor sin recursos cuando el recurso está libre (lo que normalmente solo está en el destructor de un wrapper de RAII escrito para encapsular el recurso).

Trabajé en un producto comercial grande (9 millones de declaraciones) (principalmente en C). En un momento, utilizamos macro magic para anular el puntero cuando se liberaba la memoria. Esto inmediatamente expuso a muchos bichos acechantes que fueron reparados rápidamente. Por lo que puedo recordar, nunca tuvimos un error doblemente libre.

Actualización: Microsoft cree que es una buena práctica para la seguridad y recomienda la práctica en sus políticas de SDL. Aparentemente MSVC ++ 11 pisará el puntero borrado automáticamente (en muchas circunstancias) si comstack con la opción / SDL.

En primer lugar, hay muchas preguntas existentes sobre este tema y temas estrechamente relacionados, por ejemplo, ¿Por qué no se elimina establecer el puntero a NULL? .

En su código, el problema de lo que sucede (use p). Por ejemplo, si en alguna parte tienes un código como este:

 Foo * p2 = p; 

luego, al establecer p en NULL, se logra muy poco, ya que todavía tiene que preocuparse por el puntero p2.

Esto no quiere decir que establecer un puntero a NULL sea siempre inútil. Por ejemplo, si p fuera una variable miembro que apunta a un recurso cuya duración no es exactamente la misma que la clase que contiene p, entonces establecer p en NULL podría ser una forma útil de indicar la presencia o ausencia del recurso.

Si hay más código después de la delete , sí. Cuando el puntero se elimina en un constructor o al final del método o función, No.

El objective de esta parábola es recordarle al progtwigdor, durante el tiempo de ejecución, que el objeto ya ha sido eliminado.

Una práctica aún mejor es usar Smart Pointers (compartidos o de scope) que eliminen automágicamente sus objetos de destino.

Como han dicho otros, delete ptr; ptr = 0; delete ptr; ptr = 0; no va a hacer que los demonios salgan volando de tu nariz. Sin embargo, fomenta el uso de ptr como una especie de bandera. El código se llena con delete y establece el puntero a NULL . El siguiente paso es dispersar if (arg == NULL) return; a través de su código para proteger contra el uso accidental de un puntero NULL . El problema ocurre una vez que las comprobaciones contra NULL convierten en su principal medio para verificar el estado de un objeto o progtwig.

Estoy seguro de que hay un olor a código con el uso de un puntero como una bandera en alguna parte, pero no he encontrado uno.

Voy a cambiar tu pregunta un poco:

¿Usarías un puntero no inicializado? ¿Sabes una que no estableciste en NULL o asignaste la memoria a la que apunta?

Hay dos escenarios donde se puede omitir establecer el puntero a NULL:

  • la variable del puntero queda fuera del scope de inmediato
  • ha sobrecargado la semántica del puntero y está utilizando su valor no solo como un puntero de memoria, sino también como una clave o valor sin formato. este enfoque, sin embargo, adolece de otros problemas.

Mientras tanto, argumentando que establecer el puntero a NULL podría ocultarme errores, suena como que se argumenta que no se debe corregir un error porque la corrección podría ocultar otro error. Los únicos errores que pueden aparecer si el puntero no está establecido en NULL serían los que intenten usar el puntero. Pero establecerlo en NULL en realidad causaría exactamente el mismo error que mostraría si lo usa con memoria liberada, ¿no?

Si no tiene otra restricción que lo obligue a establecer o no establecer el puntero a NULL después de eliminarlo (una de estas limitaciones fue mencionada por Neil Butterworth ), entonces mi preferencia personal es dejarlo.

Para mí, la pregunta no es “¿es esta una buena idea?” pero “¿qué comportamiento evitaría o permitiría que tuviera éxito al hacer esto?” Por ejemplo, si esto permite que otro código vea que el puntero ya no está disponible, ¿por qué otro código está intentando buscar los punteros liberados después de liberarlos? Por lo general, es un error.

También hace más trabajo de lo necesario y dificulta la depuración post mortem. Cuanto menos toques la memoria después de que no la necesites, más fácil será averiguar por qué algo se colgó. Muchas veces he confiado en el hecho de que la memoria está en un estado similar al de cuando se produjo un error en particular para diagnosticar y corregir dicho error.

Anular explícitamente después de la eliminación sugiere fuertemente al lector que el puntero representa algo que es conceptualmente opcional . Si hubiera visto eso, comenzaría a preocuparme de que en cualquier lugar de la fuente en que se utiliza el puntero, se pruebe primero contra NULL.

Si eso es lo que realmente quieres decir, es mejor hacerlo explícito en la fuente usando algo como boost :: optional

 optional p (new Foo); // (use p.get(), but must test p for truth first!...) delete p.get(); p = optional(); 

Pero si realmente quisieras que la gente supiera que el puntero “ha salido mal”, lanzaré en un 100% de acuerdo con aquellos que dicen que lo mejor es hacerlo fuera del scope. Luego está utilizando el comstackdor para evitar la posibilidad de malas referencias en el tiempo de ejecución.

Ese es el bebé en toda el agua de baño C ++, no debe tirarlo. 🙂

En un progtwig bien estructurado con verificación de errores adecuada, no hay razón para no asignarlo nulo. 0 destaca como un valor inválido universalmente reconocido en este contexto. Fallar duro y fallar pronto

Muchos de los argumentos en contra de asignar 0 sugieren que podría ocultar un error o complicar el flujo de control. Fundamentalmente, eso es un error en sentido ascendente (no es tu culpa (perdón por el juego de palabras malo)) u otro error en nombre del progtwigdor, tal vez incluso una indicación de que el flujo del progtwig se ha vuelto demasiado complejo.

Si el progtwigdor desea introducir el uso de un puntero que puede ser nulo como un valor especial y escribir todas las esquivas necesarias, eso es una complicación que han introducido deliberadamente. Cuanto mejor sea la cuarentena, más pronto encontrará casos de uso indebido, y menos podrán difundir en otros progtwigs.

Se pueden diseñar progtwigs bien estructurados utilizando las características de C ++ para evitar estos casos. Puede usar referencias, o simplemente puede decir “pasar / usar argumentos nulos o no válidos es un error”, un enfoque que se aplica igualmente a los contenedores, como los punteros inteligentes. Aumentar el comportamiento consistente y correcto prohíbe que estos errores lleguen lejos.

A partir de ahí, tiene un scope y contexto muy limitado donde puede existir un puntero nulo (o está permitido).

Lo mismo puede aplicarse a punteros que no son const . Seguir el valor de un puntero es trivial porque su scope es muy pequeño, y el uso incorrecto se comprueba y se define bien. Si su conjunto de herramientas e ingenieros no pueden seguir el progtwig después de una lectura rápida o si hay una verificación de errores inapropiada o un flujo de progtwig incoherente / indulgente, usted tiene otros problemas mayores.

Finalmente, es probable que el comstackdor y el entorno tengan algunas protecciones para los momentos en los que desee introducir errores (garabatear), detectar accesos a la memoria liberada y detectar otros UB relacionados. También puede introducir diagnósticos similares en sus progtwigs, a menudo sin afectar los progtwigs existentes.

Permíteme expandir lo que ya has puesto en tu pregunta.

Esto es lo que ha puesto en su pregunta, en forma de viñeta:


Establecer punteros a NULL después de eliminar no es una buena práctica universal en C ++. Hay momentos en que:

  • es una buena cosa hacer
  • y momentos en los que no tiene sentido y puede ocultar errores.

Sin embargo, ¡no hay momentos en que esto sea malo ! No introducirá más errores al anularlo explícitamente, no perderá memoria, no hará que ocurra un comportamiento indefinido .

Entonces, si tiene dudas, simplemente anule.

Habiendo dicho eso, si sientes que tienes que anular explícitamente algún puntero, entonces para mí esto parece que no has dividido un método lo suficiente, y debes mirar el enfoque de refactorización llamado “Método de extracción” para dividir el método en partes separadas.

Sí.

El único “daño” que puede hacer es introducir ineficiencia (una operación de tienda innecesaria) en su progtwig, pero esta sobrecarga será insignificante en relación con el costo de asignar y liberar el bloque de memoria en la mayoría de los casos.

Si no lo haces, tendrás algunos desagradables errores de eliminación de puntero algún día.

Siempre uso una macro para eliminar:

 #define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; } 

(y similar para una matriz, libre (), mangos de liberación)

También puede escribir métodos de “auto eliminación” que toman como referencia el puntero del código de llamada, por lo que fuerzan el puntero del código de llamada a NULL. Por ejemplo, para eliminar un subárbol de muchos objetos:

 static void TreeItem::DeleteSubtree(TreeItem *&rootObject) { if (rootObject == NULL) return; rootObject->UnlinkFromParent(); for (int i = 0; i < numChildren) DeleteSubtree(rootObject->child[i]); delete rootObject; rootObject = NULL; } 

editar

Sí, estas técnicas infringen algunas reglas sobre el uso de macros (y sí, en estos días probablemente puedas obtener el mismo resultado con las plantillas), pero al usar durante muchos años nunca accedí a la memoria muerta, una de las más desagradables y difíciles. La mayor cantidad de tiempo para depurar problemas que puede enfrentar. En la práctica, durante muchos años, han eliminado una gran variedad de errores de cada equipo en el que los he introducido.

También hay muchas maneras de implementar lo anterior: solo bash ilustrar la idea de forzar a las personas a NULL un puntero si eliminan un objeto, en lugar de proporcionarles un medio para liberar la memoria que no anula el puntero de la persona que llama .

Por supuesto, el ejemplo anterior es solo un paso hacia un puntero automático. Lo cual no sugerí porque el OP preguntaba específicamente sobre el caso de no usar un puntero automático.

“Hay momentos en que es bueno hacer algo, y momentos en los que no tiene sentido y puede ocultar errores”

Puedo ver dos problemas: ese código simple:

 delete myObj; myobj = 0 

se convierte en un for-liner en un entorno multiproceso:

 lock(myObjMutex); delete myObj; myobj = 0 unlock(myObjMutex); 

La “mejor práctica” de Don Neufeld no se aplica siempre. Por ejemplo, en un proyecto automotriz tuvimos que establecer punteros a 0 incluso en destructores. Me imagino que en un software crítico para la seguridad, tales reglas no son infrecuentes. Es más fácil (y sabio) seguirlos que tratar de persuadir al equipo / verificador de código para cada uso de puntero en el código, que una línea que anula este puntero es redundante.

Otro peligro es confiar en esta técnica en el código de excepciones:

 try{ delete myObj; //exception in destructor myObj=0 } catch { //myObj=0; < - possibly resource-leak } if (myObj) // use myObj <--undefined behaviour 

En dicho código, o bien produce una fuga de recursos y pospone el problema o el proceso falla.

Por lo tanto, estos dos problemas que pasan espontáneamente por mi cabeza (Herb Sutter seguramente me dirá más) me hacen obsoleto todas las preguntas del tipo "Cómo evitar el uso de punteros inteligentes y hacer el trabajo de forma segura con punteros normales".

Siempre hay puntos suspensivos de los que preocuparse.

Si va a reasignar el puntero antes de usarlo nuevamente (desreferenciarlo, pasarlo a una función, etc.), hacer que el puntero sea NULO es solo una operación adicional. Sin embargo, si no está seguro de si se reasignará o no antes de volver a utilizarlo, establecerlo en NULL es una buena idea.

Como muchos han dicho, por supuesto es mucho más fácil usar punteros inteligentes.

Editar: Como dijo Thomas Matthews en esta respuesta anterior , si se borra un puntero en un destructor, no es necesario asignarle NULL, ya que no se volverá a utilizar porque el objeto ya se está destruyendo.

Puedo imaginar el establecimiento de un puntero a NULL después de eliminarlo siendo útil en casos excepcionales en los que existe un escenario legítimo de reutilización en una única función (u objeto). De lo contrario, no tiene sentido, un puntero necesita apuntar a algo significativo mientras exista – punto.

Si el código no pertenece a la parte más crítica para el rendimiento de su aplicación, manténgalo simple y use un shared_ptr:

 shared_ptr p(new Foo); //No more need to call delete 

Realiza recuentos de referencia y es seguro para subprocesos. Puede encontrarlo en tr1 (std :: tr1 namespace, #include ) o si su comstackdor no lo proporciona, consígalo de boost.