c++ - Comportamiento observable y comportamiento indefinido: ¿qué sucede si no llamo un destructor?
c++> class constructor (12)
Nota: He visto preguntas similares, pero ninguna de las respuestas es lo suficientemente precisa, así que me lo pregunto yo mismo.
Esta es una pregunta de "lenguaje-abogado" muy delicada; Estoy buscando una respuesta autorizada.
El estándar de C ++ dice:
Un programa puede terminar la vida útil de cualquier objeto reutilizando el almacenamiento que el objeto ocupa o llamando explícitamente al destructor para un objeto de un tipo de clase con un destructor no trivial. Para un objeto de un tipo de clase con un destructor no trivial, no se requiere que el programa llame al destructor explícitamente antes de que el almacenamiento que ocupa el objeto se reutilice o libere; sin embargo, si no hay una llamada explícita al destructor o si no se usa una expresión de eliminación para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier programa que dependa de los efectos secundarios producidos por el destructor tendrá un comportamiento indefinido .
Simplemente no entiendo lo que significa "depende de los efectos secundarios" .
La pregunta general es:
¿Olvidarse de llamar a un destructor es diferente a olvidar llamar a una función ordinaria con el mismo cuerpo?
Un ejemplo específico para ilustrar mi punto es:
Considere un programa como este a continuación. También considere las variaciones obvias (por ejemplo, qué pasa si no construyo un objeto encima de otro, pero aún me olvido de llamar al destructor, qué sucede si no imprimo la salida para observarlo, etc.):
#include <math.h>
#include <stdio.h>
struct MakeRandom
{
int *p;
MakeRandom(int *p) : p(p) { *p = rand(); }
~MakeRandom() { *p ^= rand(); }
};
int main()
{
srand((unsigned) time(NULL)); // Set a random seed... not so important
// In C++11 we could use std::random_xyz instead, that''s not the point
int x = 0;
MakeRandom *r = new MakeRandom(&x); // Oops, forgot to call the destructor
new (r) MakeRandom(&x); // Heck, I''ll make another object on top
r->~MakeRandom(); // I''ll remember to destroy this one!
printf("%d", x); // ... so is this undefined behavior!?!
// If it''s indeed UB: now what if I didn''t print anything?
}
Me parece ridículo decir que esto exhibe un "comportamiento indefinido", porque x
ya es aleatorio, y por lo tanto, XOR en otro número aleatorio no puede hacer que el programa sea más "indefinido" que antes, ¿verdad?
Además, ¿en qué punto es correcto decir que el programa "depende" del destructor? ¿Lo hace si el valor es aleatorio, o en general, si no hay forma de que distinga el destructor del funcionamiento y el funcionamiento? ¿Qué pasa si nunca leo el valor? Básicamente:
¿Bajo qué condición (s), si existe alguna, muestra este programa un comportamiento indefinido?
¿Exactamente qué expresión (s) o declaración (s) causa esto, y por qué?
Simplemente no entiendo lo que significa "depende de los efectos secundarios".
Significa que depende de algo que el destructor está haciendo. En su ejemplo, modificar *p
o no modificarlo. Tiene esa dependencia en su código, ya que la salida sería diferente si no se llamara al dctor.
En su código actual, el número que se imprime, podría no ser el mismo que habría devuelto la segunda llamada rand (). Su programa invoca un comportamiento indefinido, pero es solo que UB aquí no tiene ningún efecto negativo.
Si no imprimiera el valor (o lo leyera de otra manera), entonces no habría ninguna dependencia de los efectos secundarios del dcor, y por lo tanto no habrá UB.
Asi que:
¿Olvidarse de llamar a un destructor es diferente a olvidar llamar a una función ordinaria con el mismo cuerpo?
No, no es diferente a este respecto. Si depende de que se le llame, debe asegurarse de que se llame, de lo contrario no se satisface su dependencia.
Además, ¿en qué punto es correcto decir que el programa "depende" del destructor? ¿Lo hace si el valor es aleatorio, o en general, si no hay forma de que distinga el destructor del funcionamiento y el funcionamiento?
Al azar o no, no importa, porque el código depende de la variable que se está escribiendo. El hecho de que sea difícil predecir cuál es el nuevo valor no significa que no haya dependencia.
¿Qué pasa si nunca leo el valor?
Entonces no hay UB, ya que el código no depende de la variable después de que se escribió.
¿Bajo qué condición (s), si existe alguna, muestra este programa un comportamiento indefinido?
No hay condiciones. Siempre es UB.
¿Exactamente qué expresión (s) o declaración (s) causa esto, y por qué?
La expresion:
printf("%d", x);
Porque introduce la dependencia de la variable afectada.
En los comentarios has dejado una pregunta simple que me hizo replantearme lo que dije. He eliminado la respuesta anterior porque, aunque tuviera algún valor, estaba lejos del punto.
Entonces, ¿está diciendo que mi código está bien definido, ya que "no depende de eso, incluso si lo imprimo"? No hay comportamiento indefinido aquí?
Permítanme decir nuevamente que no recuerdo con precisión la definición de nuevo operador de ubicación y las reglas de desasignación. En realidad, ni siquiera he leído el último estándar de C ++ completo. Pero si el texto que citaste es de allí, entonces estás golpeando la UB.
No debido a Rand o Imprimir. O cualquier cosa que "veamos".
Cualquier UB que ocurra aquí se debe a que su código asume que puede "sobrescribir" con seguridad un "objeto" antiguo sin destruir la instancia anterior que estaba en ese lugar. El efecto secundario principal de un destructor no es "liberar los mangos / recursos" (¡lo que usted hace manualmente en su código!), Sino dejar el espacio "listo para ser reclamado / reutilizado".
Ha asumido que el uso de los fragmentos de memoria y la vida útil de los objetos no están bien rastreados. Estoy bastante seguro de que el estándar de C ++ no define que están sin seguimiento .
Por ejemplo, imagine que tiene el mismo código que se proporciona, pero que esta estructura / clase tiene un vtable
. Imagine que está utilizando un compilador hiper-exigente que tiene toneladas de debugchecks que gestiona el vtable con mucho cuidado y asigna un poco más de bitflag y que inyecta código en constructores y destructores de base que voltea esa bandera para ayudar a rastrear errores. En dicho compilador, este código se bloquearía en la línea de new (r) MakeRandom
ya que la vida útil del primer objeto no se ha terminado. Y estoy bastante seguro de que un compilador tan exigente seguirá siendo totalmente compatible con C ++, al igual que su compilador también lo es.
Es una UB. Es solo que la mayoría de los compiladores realmente no hacen tales controles.
En primer lugar, debemos definir un comportamiento indefinido, que de acuerdo con las preguntas frecuentes de C sería cuando:
Cualquier cosa puede suceder; La Norma no impone requisitos. Es posible que el programa no se compile, o que se ejecute incorrectamente (ya sea fallando o generando resultados incorrectos de manera silenciosa), o puede, por casualidad, hacer exactamente lo que el programador pretendía.
Lo que, en otras palabras, significa que el programador no puede predecir lo que sucedería una vez que se ejecute el programa. Esto no significa que el programa o sistema operativo se bloquee, simplemente significa que el estado futuro del programa solo se sabrá una vez que se ejecute.
Entonces, explicado en notación matemática, si un programa se reduce a una función F que realiza una transformación de un estado inicial a un estado final Fs , dadas ciertas condiciones iniciales Ic
F (Is, Ic) -> Fs
Y si evalúa la función (ejecuta el programa) n veces, dado que n-> ∞
F (Is, Ic) -> Fs1 , F (Is, Ic) -> Fs2 , ..., F (Is, Ic) -> Fsn , n-> ∞
Entonces:
- Un comportamiento definido estaría dado por todos los estados resultantes siendo los mismos: Fs1 = Fs2 = ... = Fsn, dado que n-> ∞
- Un comportamiento indefinido estaría dado por la posibilidad de obtener diferentes estados finales entre diferentes ejecuciones. Fs1 ≠ Fs2 ≠ ... ≠ Fsn, dado que n-> ∞
Observe cómo resalto la posibilidad , porque el comportamiento indefinido es exactamente eso. Existe la posibilidad de que el programa se ejecute según lo deseado, pero nada garantiza que lo haría, o que no lo haría.
Por lo tanto, respondiendo a su respuesta:
¿Olvidarse de llamar a un destructor es diferente a olvidar llamar a una función ordinaria con el mismo cuerpo?
Dado que un destructor es una función que podría llamarse incluso cuando no se lo llama explícitamente, olvidarse de llamar a un destructor ES diferente de olvidar llamar a una función ordinaria, y hacerlo PODRÍA llevar a un comportamiento indefinido.
La justificación viene dada por el hecho de que, cuando olvida llamar a una función ordinaria, está SEGURO, de antemano, que esa función no se activará en ningún punto de su programa, incluso cuando ejecuta su programa un número infinito de veces.
Sin embargo, cuando olvida llamar a un destructor, y llama a su programa un número infinito de veces, y como se ilustra en esta publicación: https://.com/questions/3179494/under-what-circumstances-are-c-destructors-not-going-to-be-called ciertas circunstancias, los destructores no se van a llamar, no se llaman a los destructores de C ++, significa que no se puede asegurar de antemano cuándo se llamará al destructor, ni cuándo. Esta incertidumbre significa que no se puede asegurar el mismo estado final, lo que lleva a UB.
Así que respondiendo a tu segunda pregunta:
¿Bajo qué condición (s), si existe alguna, muestra este programa un comportamiento indefinido?
Las circunstancias serían dadas por las circunstancias en que no se llaman los destructores de C ++, dado en el enlace al que hice referencia.
Esto tiene sentido si acepta que el Estándar requiere que la asignación se equilibre con la destrucción en el caso en que los destructores afecten el comportamiento del programa. Es decir, la única interpretación plausible es que si un programa
- alguna vez falla al llamar al destructor (tal vez indirectamente a través de la
delete
) en un objeto y - dicho destructor tiene efectos secundarios,
Entonces el programa está condenado a la tierra de la UB. (OTOH, si el destructor no afecta el comportamiento del programa, está descolgado. Puede omitir la llamada).
Nota agregada Los efectos secundarios se discuten en este artículo de SO , y no lo repetiré aquí. Una inferencia conservadora es que "el programa ... depende del destructor" es equivalente a "el destructor tiene un efecto secundario".
Nota adicional Sin embargo, la Norma parece permitir una interpretación más liberal. No define formalmente la dependencia de un programa. (Define una calidad específica de las expresiones como portadora de dependencia, pero esto no se aplica aquí.) Sin embargo, en más de 100 usos de derivados de "A depende de B" y "A tiene una dependencia de B", emplea el método convencional. Sentido de la palabra: una variación en B conduce directamente a la variación en A. En consecuencia, no parece un salto inferir que un programa P depende del efecto secundario E en la medida en que el rendimiento o el incumplimiento de E resulten en una variación. En comportamiento observable durante la ejecución de p . Aquí estamos en tierra firme. El significado de un programa, su semántica, es equivalente según el Estándar a su comportamiento observable durante la ejecución, y esto está claramente definido.
Los requisitos mínimos para una implementación conforme son:
El acceso a objetos volátiles se evalúa estrictamente de acuerdo con las reglas de la máquina abstracta.
Al finalizar el programa, todos los datos escritos en archivos serán idénticos a uno de los posibles resultados que la ejecución del programa de acuerdo con la semántica abstracta hubiera producido.
La dinámica de entrada y salida de los dispositivos interactivos se llevará a cabo de tal manera que la salida de solicitud se envíe realmente antes de que un programa espere la entrada. Lo que constituye un dispositivo interactivo está definido por la implementación.
Estos colectivamente se conocen como el comportamiento observable del programa.
Por lo tanto, según las convenciones del Estándar, si el efecto secundario de un destructor afectara en última instancia el acceso, la entrada o la salida del almacenamiento volátil, y ese destructor nunca se llame, el programa tiene UB.
Dicho de otra manera: si sus destructores hacen cosas importantes y no son llamadas constantemente, su programa (dice el Estándar) debe considerarse, y por lo tanto se declara inútil.
¿Es esto demasiado restrictivo, no pedante, para un estándar de lenguaje? (Después de todo, el Estándar evita que los efectos secundarios se produzcan debido a una llamada implícita del destructor y luego te arrastra si el destructor hubiera causado una variación en el comportamiento observable si se hubiera llamado). Quizás así. Pero tiene sentido como una manera de insistir en programas bien formados.
Esto, de hecho, no es una cosa muy bien definida en el estándar, pero interpretaría que "depende de" como "el comportamiento de las reglas de la máquina abstracta está afectado".
Este comportamiento consiste en la secuencia de lecturas y escrituras en variables volátiles y las llamadas a las funciones de E / S de la biblioteca (que incluye al menos las funciones de E / S de la biblioteca estándar como printf
, pero también puede incluir cualquier número de funciones adicionales en cualquier implementación dada, por ejemplo, funciones de WinAPI). Ver 1.9 / 9 para la redacción exacta.
Por lo tanto, el comportamiento no está definido si la ejecución del destructor o la falta del mismo afecta este comportamiento. En su ejemplo, si el destructor se ejecuta o no afecta el valor de x
, pero esa tienda está muerta de todos modos, ya que la siguiente llamada del constructor lo sobrescribe, por lo que el compilador podría realmente optimizarlo (y es probable que así sea). Pero lo que es más importante, la llamada a rand()
afecta el estado interno del RNG, que influye en los valores devueltos por rand()
en el constructor y destructor del otro objeto, por lo que afecta el valor final de x
. Es "aleatorio" (pseudoaleatorio) de cualquier manera, pero sería un valor diferente. Luego imprime x
, convirtiendo esa modificación en un comportamiento observable, lo que hace que el programa quede indefinido.
Si nunca hiciste nada observable con x
o el estado RNG, el comportamiento observable no cambiaría independientemente de si se llama al destructor o no, por lo que no estaría indefinido.
Para esta respuesta, usaré una versión 2012 C ++ 11 del estándar C ++, que se puede encontrar aquí (estándar C ++) , ya que está disponible de forma gratuita y actualizada.
Los siguientes tres términos utilizados en su pregunta aparecen como sigue:
- Destructor - 385 veces
- Efecto secundario - 71 veces
- Depende - 41 veces
Lamentablemente, "depende del efecto secundario" aparece solo una vez, y DEPENDS ON no es un identificador estandarizado de RFC como DEBE, por lo que es bastante difícil determinar qué significa.
Depende de
Tomemos un enfoque de "juez activista" y asumamos que "dependiente", "dependencia" y "dependiente" se usan en un contexto similar en este documento, es decir, que el lenguaje se usó para transmitir una idea amplia en lugar de Transmitir un concepto de legalidad.
Entonces podemos analizar esta porción de la página 1194:
17.6.3.2
Efecto sobre la característica original: cambio de función movido a un encabezado diferente
Justificación: Eliminar la dependencia en para swap.
Efecto sobre la característica original: el código válido de C ++ 2003 que se ha compilado esperando que el swap esté en <algoritmo> debe incluir en su lugar <utilidad>.
Esta parte indica un tipo estricto de dependencia; originalmente necesitabas incluir para obtener std :: swap. "depende de", por lo tanto, indica un requisito estricto, una necesidad por así decirlo, en el sentido de que no hay contexto suficiente sin el requisito de proceder; El fracaso se producirá sin la dependencia.
Elegí este pasaje porque transmite el significado intencionado lo más claramente posible; otros pasajes son más detallados, pero todos incluyen un significado similar: necesidad .
Por lo tanto, una relación "depende de" significa que la cosa de la que se depende es necesaria para que el elemento dependiente tenga sentido, sea completo y completo, y se pueda utilizar en un contexto.
Para eliminar la burocracia, esto significa que A depende de B significa que A requiere B. Esto es básicamente lo que entendería que "dependa" significa si lo buscó en un diccionario o lo pronunció en una oración.
Efecto secundario
Esto se define más estrictamente, en la página 10:
Acceder a un objeto designado por un glvalue volátil (3.10), modificar un objeto, llamar a una función de E / S de la biblioteca o llamar a una función que realiza cualquiera de esas operaciones son todos efectos secundarios , que son cambios en el estado del entorno de ejecución.
Esto significa que cualquier cosa que provoque un cambio en el entorno (como RAM, IO de red, variables, etc.) son efectos secundarios. Esto encaja perfectamente con la noción de impureza / pureza de los lenguajes funcionales, que es claramente lo que se pretendía. Tenga en cuenta que el estándar de C ++ no requiere que tales efectos secundarios sean observables; La modificación de una variable de cualquier manera, incluso si esa variable nunca se observa, sigue siendo un efecto secundario.
Sin embargo, debido a la regla "como si", estos efectos secundarios no observables pueden eliminarse, página 8:
Una implementación conforme que ejecute un programa bien formado producirá el mismo comportamiento observable que una de las ejecuciones posibles de la instancia correspondiente de la máquina abstracta con el mismo programa y la misma entrada. Sin embargo, si alguna ejecución de este tipo contiene una operación no definida, esta Norma Internacional no impone ningún requisito a la implementación que ejecuta ese programa con esa entrada (ni siquiera con respecto a las operaciones que preceden a la primera operación no definida).
Depende de los efectos secundarios.
Al juntar estas dos definiciones, ahora podemos definir esta frase: algo depende de los efectos secundarios cuando se requieren esos cambios en el entorno de ejecución para satisfacer las operaciones completas, completas y completas del programa. Si, sin los efectos secundarios, no se cumple alguna restricción que se requiere para que el programa funcione de manera compatible, podemos decir que depende de los efectos secundarios.
Un ejemplo simple para ilustrar esto sería, como se indica en otra respuesta, un bloqueo. Un programa que utiliza bloqueos depende del efecto secundario del bloqueo , en particular, el efecto secundario de proporcionar un patrón de acceso serializado a algún recurso (simplificado). Si se viola este efecto secundario, se violan las restricciones del programa y, por lo tanto, no se puede considerar que el programa tenga sentido (ya que pueden darse condiciones de carrera u otros peligros).
El programa DEPENDE de las restricciones que proporciona un bloqueo, a través de los efectos secundarios; violando esos resultados en un programa que no es válido.
Depende de los efectos secundarios producidos por el destructor.
Cambiar el idioma de referirse a un bloqueo a un destructor es simple y obvio; Si el destructor tiene efectos secundarios que satisfacen alguna restricción que el programa exige que sea sensato, completo, completo y utilizable, entonces depende de los efectos secundarios producidos por el destructor. Esto no es exactamente difícil de entender, y se desprende con bastante facilidad de una interpretación legal de la norma y de una comprensión superficial de las palabras y cómo se utilizan.
Ahora podemos contestar sus preguntas:
¿Bajo qué condición (s), si existe alguna, muestra este programa un comportamiento indefinido?
Cada vez que no se cumple una dependencia o un requisito porque no se llama a un destructor, el comportamiento de cualquier código dependiente no está definido. Pero, ¿qué significa esto realmente?
1.3.24 comportamiento no definido
Comportamiento para el cual esta Norma Internacional no impone requisitos.[Nota: Se puede esperar un comportamiento no definido cuando esta Norma Internacional omite cualquier definición explícita de comportamiento o cuando un programa utiliza una construcción errónea o datos erróneos.
El comportamiento indefinido permisible va desde ignorar la situación completamente con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico), hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).
Muchas construcciones de programas erróneas no generan un comportamiento indefinido; se requieren para ser diagnosticados. - nota final]
Supongamos por un momento que tal comportamiento fue definido.
Supongamos que era explícitamente ilegal. Esto requeriría entonces que cualquier compilador estándar detecte este caso, lo diagnostique, lo trate de alguna manera. Por ejemplo, cualquier objeto que no se elimine explícitamente tendría que eliminarse al salir del programa, lo que requeriría algún tipo de mecanismo de seguimiento y capacidad para emitir destructores a tipos arbitrarios, posiblemente no conocidos en el momento de la compilación. Esto es básicamente un recolector de basura, pero dado que posiblemente es posible ocultar punteros, es posible llamar a malloc, etc., sería prácticamente imposible requerir esto.
Supongamos que estaba explícitamente permitido. Esto también permitiría a los compiladores eliminar llamadas de destructor, bajo la regla de "si", ya que, de todas formas, no puede depender de ese comportamiento. Esto daría lugar a algunas sorpresas desagradables, en su mayoría relacionadas con la memoria, que no se libera muy rápida o fácilmente. Para evitar eso, todos empezaríamos a usar finalizadores, y el problema surge una vez más. Además, permitir ese comportamiento significa que ninguna biblioteca puede estar segura de cuándo se recuperará su memoria o si alguna vez lo será, o si sus bloqueos, recursos dependientes del sistema operativo, etc., alguna vez se devolverán. Esto empuja los requisitos para la limpieza del código usando los recursos al código que lo proporciona, donde es básicamente imposible tratar con un lenguaje como C o C ++.
Supongamos que tenía un comportamiento específico; ¿Qué comportamiento sería este? Cualquier comportamiento de este tipo tendría que estar muy involucrado o no cubriría la gran cantidad de casos. Ya hemos cubierto dos, y la idea de limpiar cualquier objeto dado al salir del programa impone una gran sobrecarga. Para un lenguaje destinado a ser rápido o al menos mínimo, esto es claramente una carga innecesaria.
Entonces, en cambio, el comportamiento fue etiquetado como indefinido , lo que significa que cualquier implementación es libre de proporcionar diagnósticos, pero también es libre de simplemente ignorar el problema y dejar que usted lo descubra. Pero no importa qué, si dependes de que se cumplan esas restricciones pero no llamas al destructor, obtienes un comportamiento indefinido . Incluso si el programa funciona perfectamente bien, ese comportamiento es indefinido; puede lanzar un mensaje de error en una nueva versión de Clang, puede eliminar su disco duro en algún sistema operativo criptográfico increíblemente seguro del futuro lejano, puede funcionar hasta el final de los tiempos.
Pero sigue sin estar definido.
Tu ejemplo
Su ejemplo no satisface la cláusula "depende de"; ninguna restricción que se requiere para que el programa se ejecute no está satisfecha.
- El constructor requiere un puntero bien formado a una variable real: satisfecho
- nuevo requiere un búfer correctamente asignado: satisfecho
- printf requiere una variable accesible, interpretable como un entero: satisfecho
Ningún lugar en este programa hace un cierto valor para x o la falta de ese valor resulta en una restricción insatisfecha; No estás invocando un comportamiento indefinido. Nada "depende" de estos efectos secundarios; Si tuviera que agregar una prueba que funcionara como una restricción que requiriera un cierto valor para "x", entonces sería un comportamiento indefinido.
Tal como está, su ejemplo no es un comportamiento indefinido; es simplemente incorrecto
¡Finalmente!
¿Olvidarse de llamar a un destructor es diferente a olvidar llamar a una función ordinaria con el mismo cuerpo?
Es imposible en muchos casos definir una función ordinaria con el mismo cuerpo:
- Un destructor es un miembro, no una función ordinaria.
- Una función no puede acceder a valores privados o protegidos.
- Una función no puede ser requerida para ser llamada a la destrucción
- Un finalizador tampoco puede ser requerido a ser destruido.
- Una función ordinaria no puede restaurar la memoria al sistema operativo sin llamar al destructor
Y no, llamar gratis a un objeto asignado no puede restaurar la memoria; free / malloc no necesita trabajar en cosas asignadas con nuevas, y sin llamar al destructor, los miembros de datos privados no serán liberados, lo que resultará en una pérdida de memoria.
Además, olvidarse de llamar a una función no dará lugar a un comportamiento indefinido si su programa depende de los efectos secundarios que impone; esos efectos secundarios simplemente no se impondrán, y su programa no satisfará esas restricciones, y probablemente no funcionará según lo previsto. Sin embargo, olvidarse de llamar a un destructor, resulta en un comportamiento indefinido, como se indica en la página 66:
Para un objeto de un tipo de clase con un destructor no trivial, no se requiere que el programa llame al destructor explícitamente antes de que el almacenamiento que ocupa el objeto se reutilice o libere; sin embargo, si no hay una llamada explícita al destructor o si no se usa una expresión de eliminación (5.3.5) para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier programa que dependa de los efectos secundarios producidos por el destructor tiene un comportamiento indefinido.
Como mencionaste en tu pregunta original. No veo por qué tuvo que hacer la pregunta, dado que ya la mencionó, pero ya está.
Mi lectura de esta parte de la norma es:
- Se le permite reutilizar el almacenamiento para un objeto que tiene un destructor no trivial sin llamar a ese destructor
- Si lo hace, el compilador no tiene permitido llamar al destructor por usted.
- Si su programa tiene una lógica que depende del destructor que se está llamando, su programa podría fallar.
Los efectos secundarios aquí son simplemente cambios en el estado del programa que resultan de llamar al destructor. Serán cosas como actualizar recuentos de referencias, liberar bloqueos, cerrar manijas y cosas así.
''Depende de los efectos secundarios'' significa que otra parte del programa espera que el recuento de referencia se mantenga correctamente, que los bloqueos se liberen, los mangos se cierren, etc. Si practica no llamar a los destructores, debe asegurarse de que la lógica de su programa no dependa de que se haya llamado.
Si bien ''olvidar'' no es realmente relevante, la respuesta es no, los destructores son solo funciones. La diferencia clave es que, en algunas circunstancias, el compilador los llama (''implícitamente'') y esta sección de la norma define una situación en la que no lo harán.
Su ejemplo realmente no depende de los efectos secundarios. Obviamente, llama a la función aleatoria exactamente 3 veces e imprime el valor que calcula. Podrías cambiarlo así:
- La estructura mantiene un recuento de referencia (ctor +1, dtor -1)
- Una función de fábrica reutiliza objetos y llama aleatoriamente al destructor o no
- Una función de cliente ''depende de'' que el recuento de referencia se mantenga correctamente, esperando que sea cero.
Obviamente, con esta dependencia, el programa exhibirá un "comportamiento indefinido" con respecto al recuento de referencia.
Tenga en cuenta que ''comportamiento indefinido'' no tiene por qué ser un mal comportamiento. Simplemente significa "comportamiento para el cual esta Norma Internacional no impone requisitos".
Realmente creo que existe el peligro de pensar demasiado en lo que es básicamente un concepto bastante simple. No puedo citar ninguna autoridad más allá de las palabras que están aquí y el estándar en sí, lo cual me parece bastante claro (pero por todos los medios, dígame si me falta algo).
Básicamente significa que cuando define su propio destructor para una clase, ya no se llama automáticamente al salir del alcance. El objeto aún estará fuera del alcance si intentas usarlo, pero la memoria seguirá usándose en la pila y no ocurrirá nada en tu destructor no predeterminado. Si desea que el número de objetos disminuya cada vez que llame a su destructor, por ejemplo, no sucederá.
Digamos que tienes una clase que adquiere un bloqueo en su constructor y luego libera el bloqueo en su destructor. Liberar el bloqueo es un efecto secundario de llamar al destructor.
Ahora, es tu trabajo asegurarte de que se llame al destructor. Por lo general, esto se hace llamando delete
, pero también puede llamarlo directamente, y esto generalmente se hace si ha asignado un objeto utilizando una ubicación nueva.
En tu ejemplo, has asignado 2 MakeRandom
instancias, pero solo has llamado al destructor en una de ellas. Si estuviera administrando algún recurso (como un archivo), entonces tendría una fuga de recursos.
Entonces, para responder a su pregunta, sí, olvidarse de llamar a un destructor es diferente a olvidar llamar a una función ordinaria. Un destructor es el inverso del constructor. Se le exige que llame al constructor y, por lo tanto, debe llamar al destructor para "desenrollar" todo lo que haya hecho el destructor. Este no es el caso con una función "ordinaria".
No he leído los comentarios de todos los demás, pero tengo una explicación simple. En la cita
sin embargo, si no hay una llamada explícita al destructor o si no se usa una expresión de eliminación para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier programa que dependa de los efectos secundarios producidos por el destructor tendrá un comportamiento indefinido.
El significado es muy diferente dependiendo de cómo lo analices. Este significado es de lo que oigo hablar a la gente.
sin embargo, {si no hay una llamada explícita al destructor o si no se usa una expresión de eliminación para liberar el almacenamiento}, el destructor no se llamará implícitamente y cualquier programa que dependa de los efectos secundarios producidos por el destructor tendrá un comportamiento indefinido .
Pero creo que este significado tiene más sentido.
sin embargo, si no hay una llamada explícita al destructor o {si no se usa una expresión de eliminación para liberar el almacenamiento, el destructor no se llamará de manera implícita y cualquier programa que dependa de los efectos secundarios producidos por el destructor no tendrá un comportamiento definido) .
que básicamente dice que C ++ no tiene un recolector de basura y si usted asume que tiene GC, su programa no funcionará como espera.
Se requiere que el estándar hable en términos tales como observable behavior
y side effects
porque, aunque muchas personas olvidan esto, c ++ no solo se usa para software de PC.
Considera el ejemplo en tu comentario a la respuesta de Gene:
class S {
unsigned char x;
public: ~S() {
++x;
}
};
El destructor aquí está claramente modificando un objeto (de ahí que sea un "efecto secundario" con la definición dada), pero estoy bastante seguro de que ningún programa podría "depender" de este efecto secundario en un sentido razonable del término. ¿Qué me estoy perdiendo?
te estás perdiendo el mundo incrustado, por ejemplo. Considere un programa c ++ de metal abierto que se ejecuta en un procesador pequeño con acceso de registro de función especial a un uart:
new (address_of_uart_tx_special_function_register) S;
Aquí llamar al destructor claramente tiene efectos secundarios observables. Si no lo llamamos, el UART transmite un byte menos.
Por lo tanto, si los efectos secundarios son observables también depende de lo que el hardware esté haciendo con las escrituras en ciertas ubicaciones de memoria.
También puede ser digno de mencionar que incluso si el cuerpo de un destructor está vacío, podría tener efectos secundarios si alguna de las variables miembro de la clase tiene destructores con efectos secundarios.
No veo nada que le prohíba al compilador hacer otra contabilidad (tal vez con respecto a las excepciones y el desenrollado de pila). Incluso si ningún compilador lo hace actualmente y ningún compilador lo hará desde el punto de vista de un abogado lingüístico, todavía tiene que considerarlo UB a menos que sepa que el compilador no crea efectos secundarios.
Si un programa "depende de los efectos secundarios producidos por un destructor" depende de la definición de " comportamiento observable ".
Para citar el standard (sección 1.9.8, Ejecución del programa, se agrega una cara en negrita):
Los requisitos mínimos para una implementación conforme son:
- El acceso a objetos volátiles se evalúa estrictamente de acuerdo con las reglas de la máquina abstracta.
- Al finalizar el programa, todos los datos escritos en los archivos serán idénticos a uno de los posibles resultados que la ejecución del programa de acuerdo con la semántica abstracta hubiera producido.
- La dinámica de entrada y salida de los dispositivos interactivos se llevará a cabo de tal manera que la salida de solicitud se envíe realmente antes de que un programa espere la entrada. Lo que constituye un dispositivo interactivo está definido por la implementación.
Estos colectivamente se conocen como el comportamiento observable del programa. [Nota: cada implementación puede definir correspondencias más rigurosas entre la semántica abstracta y la real. ]
En cuanto a tu otra pregunta:
¿Olvidarse de llamar a un destructor es diferente a olvidar llamar a una función ordinaria con el mismo cuerpo?
¡Sí!Olvidar una llamada "equivalente" a una función conduce a un comportamiento bien definido (lo que sea que se suponía que debía hacer que no sucediera), pero es bastante diferente para un destructor. En esencia, el estándar dice que si diseña su programa de modo que un destructor observable se "olvide", entonces ya no está escribiendo C ++, y el resultado de su programa es completamente indefinido.
Edit: Oh cierto, la última pregunta:
¿Bajo qué condición (s), si existe alguna, muestra este programa un comportamiento indefinido?
Creo que printf califica como escritura en un archivo, y por lo tanto es observable. Por supuesto, rand () no es realmente aleatorio, sino que es completamente determinista para cualquier semilla dada, por lo que el programa tal como está escrito exhibe un comportamiento indefinido (dicho esto, me sorprendería mucho si no funcionara exactamente como está escrito, simplemente no lo hace). no hay que).