¿La memoria tiene un problema de clase de “comportamiento indefinido” en C ++?

Resulta que muchas cosas que parecen inocentes son un comportamiento indefinido en C ++. Por ejemplo, una vez que se ha delete puntero no nulo, incluso la impresión de ese valor del puntero es un comportamiento indefinido .

Ahora las filtraciones de memoria son definitivamente malas. Pero, ¿en qué situación de clase están definidos, indefinidos o qué otra clase de comportamiento?

Pérdidas de memoria.

No hay un comportamiento indefinido Es perfectamente legal perder memoria.

Comportamiento no definido: son acciones que el estándar específicamente no desea definir y deja hasta la implementación para que sea flexible para realizar ciertos tipos de optimizaciones sin romper el estándar.

La administración de la memoria está bien definida.
Si dinámicamente asigna memoria y no la libera. Entonces la memoria sigue siendo propiedad de la aplicación para administrarla como lo considere conveniente. El hecho de que haya perdido todas las referencias a esa parte de la memoria no está ni aquí ni allí.

Por supuesto, si continúa filtrando, eventualmente se quedará sin memoria disponible y la aplicación comenzará a lanzar excepciones bad_alloc. Pero ese es otro problema.

Las memory leaks definitivamente se definen en C / C ++.

Si lo hago:

 int *a = new int[10]; 

seguido por

 a = new int[10]; 

Definitivamente estoy filtrando la memoria ya que no hay forma de acceder al primer arreglo asignado y esta memoria no se libera automáticamente ya que el GC no es compatible.

Pero las consecuencias de esta fuga son impredecibles y variarán de una aplicación a otra y de una máquina a otra para una misma aplicación. Supongamos que una aplicación que se cuelga debido a una fuga en una máquina podría funcionar perfectamente en otra máquina con más RAM. También para una aplicación determinada en una máquina determinada, el locking debido a una fuga puede aparecer en diferentes momentos durante la ejecución.

Si pierde memoria, la ejecución continúa como si nada sucediera. Este es un comportamiento definido.

Al final de la pista, puede encontrar que una llamada a malloc falla debido a que no hay suficiente memoria disponible. Pero este es un comportamiento definido de malloc , y las consecuencias también están bien definidas: la llamada malloc devuelve NULL .

Ahora bien, esto puede provocar que un progtwig que no verifique el resultado de malloc falle con una violación de segmentación. Pero ese comportamiento indefinido es (desde el POV de las especificaciones del lenguaje) debido a que el progtwig desreferencia un puntero no válido, no la fuga de memoria anterior o la llamada malloc fallida.

Mi interpretación de esta statement:

Para un objeto de un tipo de clase con un destructor no trivial, no es necesario que el progtwig llame al destructor explícitamente antes de que se reutilice o libere el almacenamiento que ocupa el objeto; sin embargo, si no hay una llamada explícita al destructor o si una expresión de eliminación (5.3.5) no se utiliza para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier progtwig que dependa de los efectos secundarios producidos por el destructor tiene un comportamiento indefinido

es como sigue:

Si de alguna manera logras liberar el almacenamiento que ocupa el objeto sin llamar al destructor en el objeto que ocupó la memoria, UB es la consecuencia, si el destructor no es trivial y tiene efectos secundarios.

Si el new asigna con malloc , el almacenamiento sin procesar podría liberarse con free() , el destructor no se ejecutaría y se produciría UB. O si un puntero se convierte en un tipo no relacionado y se elimina, la memoria se libera, pero se ejecuta el destructor incorrecto, UB.

Esto no es lo mismo que una delete omitida, donde la memoria subyacente no se libera. Omitir delete no es UB.

(Comente a continuación “Heads-up: esta respuesta se ha movido aquí de ¿Tiene una pérdida de memoria causa un comportamiento indefinido? ” – probablemente tendrá que leer esa pregunta para obtener el fondo adecuado para esta respuesta O_o).

Me parece que esta parte del Estándar permite explícitamente:

  • tener un grupo de memoria personalizado en el que coloca new objetos, luego liberar / reutilizar todo sin perder tiempo llamando a sus destructores, siempre y cuando no dependa de los efectos secundarios de los objetos destructores .

  • bibliotecas que asignan un poco de memoria y nunca la liberan, probablemente porque sus funciones / objetos podrían ser utilizados por destructores de objetos estáticos y manejadores de entrada registrados, y no vale la pena comprar todo el orden orquestado de destrucción o transitorio renacimiento “phoenix” como cada vez que esos accesos suceden.

No puedo entender por qué el Estándar elige dejar el comportamiento indefinido cuando hay dependencias en los efectos secundarios, en lugar de simplemente decir que esos efectos secundarios no habrán sucedido y dejar que el progtwig tenga un comportamiento definido o indefinido como normalmente esperaría dado. esa premisa.

Todavía podemos considerar lo que el Estándar dice que es un comportamiento indefinido. La parte crucial es:

“depende de los efectos secundarios producidos por el destructor tiene un comportamiento indefinido”.

La Norma §1.9 / 12 define explícitamente los efectos colaterales de la siguiente manera (las cursivas a continuación son las Normas, que indican la introducción de una definición formal):

Acceder a un objeto designado por un glvalue volatile (3.10), modificar un objeto, llamar a una función de E / S de biblioteca o llamar a una función que realiza cualquiera de esas operaciones son todos efectos colaterales , que son cambios en el estado del entorno de ejecución.

En su progtwig, no hay dependencia así que no hay un comportamiento indefinido.

Un ejemplo de dependencia que podría coincidir con el escenario en §3.8 p4, donde la necesidad o la causa del comportamiento indefinido no es evidente, es:

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

Una cuestión que la gente está debatiendo es si el objeto X arriba se consideraría released a los fines de 3.8 p4, dado que probablemente solo se libere al SO después de la finalización del progtwig; no está claro al leer el Estándar si esa etapa del proceso de "vida" está dentro del scope de los requisitos de comportamiento del estándar (mi búsqueda rápida del estándar no aclaraba esto). Personalmente, me atrevo a decir que 3.8p4 se aplica aquí, en parte porque mientras sea lo suficientemente ambiguo como para argumentar, un comstackdor puede sentirse autorizado a permitir un comportamiento indefinido en este escenario, pero incluso si el código anterior no constituye la liberación, el escenario es fácil. ala enmendada ...

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

De todos modos, sin embargo main's implementado el destructor anterior tiene un efecto secundario - por "llamar a una función de E / S de la biblioteca"; además, el comportamiento observable del progtwig podría decirse que "depende de" ello en el sentido de que los almacenamientos intermedios que se verían afectados por el destructor si se hubiera ejecutado se enjuagan durante la terminación. ¿Pero "depende de los efectos secundarios" solo pretende aludir a situaciones en las que el progtwig claramente tendría un comportamiento indefinido si el destructor no se ejecutara? Erraría del lado de la primera, particularmente porque este último caso no necesitaría un párrafo dedicado en la Norma para documentar que el comportamiento no está definido. Aquí hay un ejemplo con un comportamiento obviamente no definido:

 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 } 

Cuando se llama al destructor x durante la terminación, b_ será false y ~X() delete p_ por un puntero ya liberado, creando un comportamiento indefinido. Si x.~X(); había sido llamado antes de la reutilización, p_ se habría establecido en 0 y la eliminación habría sido segura. En ese sentido, se podría decir que el comportamiento correcto del progtwig depende del destructor, y el comportamiento es claramente indefinido, pero acabamos de diseñar un progtwig que coincida con el comportamiento descrito de 3.8p4 en sí mismo, en lugar de tener el comportamiento como una consecuencia de 3.8p4 ...?

Escenarios más sofisticados con problemas, demasiado largos para proporcionar código, pueden incluir, por ejemplo, una biblioteca extraña de C ++ con contadores de referencia dentro de objetos de flujo de archivos que tuvieron que pulsar 0 para desencadenar algunos procesos como la descarga de E / S o la unión de hilos de fondo, etc. donde no hacerlo arriesga no solo la ejecución de la salida solicitada explícitamente por el destructor, sino también falla en la salida de otra salida de la secuencia, o en algunos sistemas operativos con un sistema de archivos transaccional podría resultar en una reversión de la E / S anterior - tales problemas podrían cambiar el comportamiento observable del progtwig o incluso dejar el progtwig colgado.

Nota: no es necesario demostrar que haya un código real que se comporte de manera extraña en cualquier comstackdor / sistema existente; el Estándar se reserva claramente el derecho para que los comstackdores tengan un comportamiento indefinido ... eso es todo lo que importa. Esto no es algo que pueda razonar y opte por ignorar el Estándar; puede ser que C ++ 14 o alguna otra revisión modifique esta estipulación, pero mientras exista, entonces si hay incluso una posible "dependencia" de los efectos secundarios, entonces existe la posibilidad de un comportamiento indefinido (que, por supuesto, sí puede ser definido por un comstackdor / implementación particular, por lo que no significa automáticamente que cada comstackdor está obligado a hacer algo extraño).

La especificación del lenguaje no dice nada sobre “pérdidas de memoria”. Desde el punto de vista del lenguaje, cuando crea un objeto en la memoria dinámica, está haciendo exactamente eso: está creando un objeto anónimo con duración ilimitada de por vida / almacenamiento. “Ilimitado” en este caso significa que el objeto solo puede finalizar su duración / duración de almacenamiento cuando lo desasigna explícitamente, pero de lo contrario continuará viviendo para siempre (siempre que el progtwig se ejecute).

Ahora, generalmente consideramos que un objeto asignado dinámicamente se convierte en una “fuga de memoria” en el punto en la ejecución del progtwig cuando todas las referencias (“referencias” genéricas, como punteros) a ese objeto se pierden hasta el punto de ser irrecuperables. Tenga en cuenta que incluso para un ser humano, la noción de “todas las referencias se pierden” no está definida con precisión. ¿Qué pasa si tenemos una referencia a alguna parte del objeto, que puede ser “recalculada” teóricamente a una referencia al objeto completo? ¿Es una pérdida de memoria o no? ¿Qué pasa si no tenemos referencias al objeto en absoluto, pero de alguna manera podemos calcular dicha referencia usando otra información disponible para el progtwig (como una secuencia precisa de asignaciones)?

La especificación del lenguaje no se ocupa de cuestiones como esa. Independientemente de lo que considere una apariencia de “pérdida de memoria” en su progtwig, desde el punto de vista del lenguaje no es un evento en absoluto. Desde el punto de vista del lenguaje, un objeto “filtrado” dinámicamente asignado simplemente continúa viviendo felizmente hasta que el progtwig finaliza. Este es el único punto de preocupación que queda: ¿qué sucede cuando el progtwig finaliza y todavía se asigna una memoria dinámica?

Si no recuerdo mal, el idioma no especifica qué sucede con la memoria dinámica que todavía está asignada en el momento de la finalización del progtwig. No se intentarán destruir / desasignar automáticamente los objetos que creó en la memoria dinámica. Pero no hay un comportamiento formal indefinido en casos como ese.

La carga de la evidencia está en aquellos que pensarían que una fuga de memoria podría ser C ++ UB.

Naturalmente, no se ha presentado ninguna evidencia.

En resumen, para cualquier persona que albergue alguna duda, esta pregunta nunca puede ser resuelta claramente, excepto amenazando al comité de manera creíble con, por ejemplo, la música fuerte de Justin Bieber, para que agreguen una statement de C ++ 14 que aclare que no es UB.


En cuestión es C ++ 11 §3.8 / 4:

Para un objeto de un tipo de clase con un destructor no trivial, no es necesario que el progtwig llame al destructor explícitamente antes de que se reutilice o libere el almacenamiento que ocupa el objeto; sin embargo, si no hay una llamada explícita al destructor o si una expresión de eliminación (5.3.5) no se utiliza para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier progtwig que dependa de los efectos secundarios producidos por el destructor tiene un comportamiento indefinido

Este pasaje tenía exactamente la misma redacción en C ++ 98 y C ++ 03. Qué significa eso?

  • no es necesario que el progtwig llame al destructor explícitamente antes de que se reutilice o libere el almacenamiento que ocupa el objeto

    – significa que uno puede tomar la memoria de una variable y reutilizar esa memoria, sin destruir primero el objeto existente.

  • si no hay una llamada explícita al destructor o si una expresión de eliminación (5.3.5) no se utiliza para liberar el almacenamiento, el destructor no se llamará implícitamente

    – significa que si uno no destruye el objeto existente antes de la reutilización de la memoria, entonces si el objeto es tal que se llama automáticamente a su destructor (por ejemplo, una variable automática local), entonces el progtwig tiene un comportamiento indefinido, porque ese destructor operaría en un objeto existente más largo.

  • y cualquier progtwig que dependa de los efectos secundarios producidos por el destructor tiene un comportamiento indefinido

    – No puede significar literalmente lo que dice, porque un progtwig siempre depende de cualquier efecto secundario, por la definición de efecto secundario. O en otras palabras, no hay forma de que el progtwig no dependa de los efectos secundarios, porque entonces no serían efectos secundarios.

Lo más probable es que lo que se pretendía no fuera lo que finalmente llegó a C ++ 98, por lo que lo que tenemos a mano es un defecto .

Desde el contexto se puede adivinar que si un progtwig se basa en la destrucción automática de un objeto de tipo T conocido estáticamente, donde la memoria se ha reutilizado para crear un objeto u objetos que no es un objeto T , entonces ese es un Comportamiento Indefinido.


Aquellos que han seguido el comentario pueden notar que la explicación anterior de la palabra “deberá” no es el significado que asumí antes. Como lo veo ahora, el “debe” no es un requisito en la implementación, lo que está permitido hacer. Es un requisito en el progtwig, lo que el código puede hacer.

Por lo tanto, esto es formalmente UB:

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

Pero esto está bien con una interpretación literal:

 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 principal es que con una interpretación literal del párrafo de la norma anterior, aún estaría formalmente bien si la ubicación nueva creara un objeto de un tipo diferente allí, solo por la destrucción explícita del original. Pero no sería muy práctico en ese caso. Tal vez esto esté cubierto por algún otro párrafo en el estándar, por lo que también es formalmente UB.

Y esto también está bien, usando la ubicación new de :

 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. } 

Como yo lo veo, ahora.

Su comportamiento definitivamente definido .

Considere un caso en el que el servidor se esté ejecutando y siga asignando memoria de stack y no se libere memoria, incluso si no hay uso de ella. Por lo tanto, el resultado final sería que, con el tiempo, el servidor se quedará sin memoria y se producirá un locking definitivo.

Agregando a todas las demás respuestas, un enfoque completamente diferente. Si observamos la asignación de memoria en § 5.3.4-18, podemos ver:

Si alguna parte de la inicialización del objeto descrita anteriormente 76 termina arrojando una excepción y se puede encontrar una función de desasignación adecuada, se llama a la función de desasignación para liberar la memoria en la que se estaba construyendo el objeto, después de lo cual la excepción continúa propagándose en el contexto de la nueva expresión Si no se puede encontrar una función de desasignación de asignación que no sea ambigua, la propagación de la excepción no libera la memoria del objeto. [Nota: Esto es apropiado cuando la función de asignación llamada no asigna memoria; de lo contrario, es probable que se produzca una pérdida de memoria. -Finalizar nota]

¿Podría causar UB aquí, sería mencionado, por lo que es “solo una pérdida de memoria”.

En lugares como §20.6.4-10, se menciona un posible colector de basura y un detector de fugas. Se ha pensado mucho en el concepto de punteros derivados con seguridad et.al. para poder usar C ++ con un recolector de basura (C.2.10 “Soporte mínimo para regiones recolectadas basura”).

Por lo tanto, si fuera UB simplemente perder el último puntero a algún objeto, todo el esfuerzo no tendría sentido.

Con respecto al “cuando el destructor tiene efectos secundarios que no lo ejecutan nunca UB”, diría que esto está mal, de lo contrario, las instalaciones como std::quick_exit() serían inherentemente UB.

Si el transbordador espacial debe despegar en dos minutos, y tengo la opción entre presentarlo con un código que filtra la memoria y el código que tiene un comportamiento indefinido, estoy ingresando el código que filtra la memoria.

Pero la mayoría de nosotros no suele estar en una situación así, y si lo hacemos, es probablemente por una falla más adelante en la línea. Quizás estoy equivocado, pero estoy leyendo esta pregunta como: “¿Qué pecado me llevará al infierno más rápido?”

Probablemente el comportamiento indefinido, pero en realidad ambos.

definido, ya que una pérdida de memoria se está olvidando de limpiar después de usted mismo.

por supuesto, una pérdida de memoria probablemente pueda causar un comportamiento indefinido más adelante.

Respuesta directa: El estándar no define qué sucede cuando se pierde la memoria, por lo tanto, es “indefinido”. Sin embargo, está implícitamente indefinido, lo cual es menos interesante que las cosas explícitamente indefinidas en el estándar.

Obviamente, esto no puede ser un comportamiento indefinido. Simplemente porque UB tiene que suceder en algún momento, y olvidarse de liberar memoria o llamar a un destructor no ocurre en ningún momento. Lo que ocurre es que el progtwig termina sin haber liberado memoria o llamado destructor; esto no hace que el comportamiento del progtwig, o de su terminación, sea indefinido de ninguna manera.

Dicho esto, en mi opinión, el estándar se contradice en este pasaje. Por un lado, asegura que no se llamará al destructor en este escenario, y por otro lado, dice que si el progtwig depende de los efectos secundarios producidos por el destructor, entonces tiene un comportamiento indefinido. Supongamos que las llamadas al destructor exit , entonces ningún progtwig que haga algo puede pretender ser independiente de eso, porque el efecto secundario de llamar al destructor le impediría hacer lo que de otro modo haría; pero el texto también asegura que no se llamará al destructor para que el progtwig pueda continuar sin hacer nada. Creo que la única manera razonable de leer el final de este pasaje es que si el comportamiento adecuado del progtwig requeriría que se llamara al destructor, entonces el comportamiento en realidad no está definido; esto es una observación superflua, dado que acaba de estipularse que no se llamará al destructor.

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.