¿Cuál es la diferencia conceptual entre finalmente y un destructor?

12

Primero, soy muy consciente de ¿Por qué no hay una construcción 'finalmente' en C ++? pero una discusión de comentarios de larga duración sobre otra pregunta parece justificar una pregunta separada.

Además del problema de que finallyen C # y Java básicamente pueden existir solo una vez (== 1) por alcance y un solo alcance puede tener múltiples (== n) destructores de C ++, creo que son esencialmente lo mismo. (Con algunas diferencias técnicas).

Sin embargo, otro usuario argumentó :

... Estaba tratando de decir que un dtor es inherentemente una herramienta para (Release sematics) y finalmente es inherentemente una herramienta para (Commit semantics). Si no ve por qué: considere por qué es legítimo lanzar excepciones una encima de la otra en bloques finalmente, y por qué lo mismo no es para destructores. (En cierto sentido, es una cuestión de datos versus control. Los destructores son para liberar datos, finalmente son para liberar control. Son diferentes; es lamentable que C ++ los una).

Alguien puede aclarar esto?

Martin Ba
fuente

Respuestas:

6
  • Transacción ( try)
  • Error Salida / Respuesta ( catch)
  • Error externo ( throw)
  • Error de programador ( assert)
  • Revertir (lo más cercano podría ser la protección de alcance en idiomas que los admitan de forma nativa)
  • Liberación de recursos (destructores)
  • Varios Flujo de control independiente de la transacción ( finally)

No se puede llegar a una mejor descripción finallyque el flujo de control independiente de transacciones misceláneas. No necesariamente se correlaciona tan directamente con ningún concepto de alto nivel en el contexto de una mentalidad de recuperación de errores y transacciones, especialmente en un lenguaje teórico que tiene destructores y finally.

Lo que más me falta de forma inherente es una función de lenguaje que representa directamente el concepto de deshacer los efectos secundarios externos. Los protectores de alcance en lenguajes como D son lo más parecido que se me ocurre que se acerca a representar ese concepto. Desde el punto de vista del flujo de control, una reversión en el alcance de una función particular necesitaría distinguir una ruta excepcional de una regular, al tiempo que automatiza simultáneamente la reversión implícita de cualquier efecto secundario causado por la función en caso de que la transacción falle, pero no cuando la transacción tiene éxito . Eso es bastante fácil de hacer con destructores si, por ejemplo, establecemos un valor booleano como succeededverdadero al final de nuestro bloque try para evitar la lógica de reversión en un destructor. Pero es una forma indirecta de hacer esto.

Si bien eso podría parecer que no ahorraría tanto, la reversión de los efectos secundarios es una de las cosas más difíciles de corregir (por ejemplo, lo que hace que sea tan difícil escribir un contenedor genérico seguro para excepciones).


fuente
4

En cierto modo lo son, de la misma manera que un Ferrari y un tránsito pueden usarse para picar las tiendas por una pinta de leche, a pesar de que están diseñados para diferentes usos.

Podría colocar una construcción try / finally en cada ámbito y limpiar todas las variables definidas en el ámbito en el bloque finalmente para emular un destructor de C ++. Esto es, conceptualmente, lo que hace C ++: el compilador llama automáticamente al destructor cuando una variable se sale del alcance (es decir, al final del bloque de alcance). Sin embargo, tendría que organizar su intento / finalmente para que el intento sea lo primero y finalmente lo último en cada ámbito. También tendría que definir un estándar para que cada objeto tenga un método con un nombre específico que utilice para limpiar su estado al que llamaría en el bloque finalmente, aunque supongo que podría dejar la administración de memoria normal que proporciona su idioma limpia el objeto ahora vaciado cuando lo desee.

Sin embargo, no sería lindo hacer esto, y aunque .NET introdujo IDispose como un destructor administrado manualmente, y el uso de bloques como un intento de hacer que la administración manual sea un poco más fácil, todavía no es algo que quiera hacer en la práctica .

gbjbaanb
fuente
4

Desde mi punto de vista, la principal diferencia es que un destructor en c ++ es un mecanismo implícito (invocado automáticamente) para liberar los recursos asignados, mientras que el intento ... finalmente puede usarse como un mecanismo explícito para hacerlo.

En los programas de c ++, el programador es responsable de liberar los recursos asignados. Esto generalmente se implementa en el destructor de una clase y se realiza inmediatamente cuando una variable se sale del alcance o cuando se llama a delete.

Cuando en c ++ se crea una variable local de una clase sin utilizar newlos recursos de esas instancias, el destructor libera implícitamente cuando hay una excepción.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

En Java, C # y otros sistemas con administración automática de memoria, el recolector de basura del sistema decide cuándo se destruye una instancia de clase.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

No hay un mecanismo implícito para eso, por lo que debe programar esto explícitamente usando try finalmente

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}
k3b
fuente
En C ++, ¿por qué se llama al destructor automáticamente solo con un objeto de pila y no con un objeto de montón en caso de una excepción?
Giorgio
@Giorgio Porque los recursos de almacenamiento dinámico viven en un espacio de memoria que no está directamente vinculado a la pila de llamadas. Por ejemplo, imagine una aplicación multiproceso con 2 hilos, Ay B. Si se arroja un subproceso, la A'stransacción de reversión no debe destruir los recursos asignados B, por ejemplo, los estados del subproceso son independientes entre sí, y la memoria persistente que vive en el montón es independiente de ambos. Sin embargo, normalmente en C ++, la memoria de almacenamiento dinámico todavía está vinculada a los objetos en la pila.
@Giorgio Por ejemplo, un std::vectorobjeto podría vivir en la pila pero apuntar a la memoria en el montón: tanto el objeto vectorial (en la pila) como su contenido (en el montón) se desasignarían durante una desconexión de la pila en ese caso, ya que destruir el vector en la pila invocaría un destructor que libera la memoria asociada en el montón (y también destruiría esos elementos del montón). Por lo general, para la seguridad de la excepción, la mayoría de los objetos C ++ viven en la pila, incluso si son solo identificadores que apuntan a la memoria en el montón, automatizando el proceso de liberar tanto el almacenamiento dinámico como la memoria de pila en la pila.
4

Me alegra que hayas publicado esto como una pregunta. :)

Estaba tratando de decir que los destructores y finallyconceptualmente son diferentes:

  • Los destructores son para liberar recursos ( datos )
  • finallyes para volver a la persona que llama ( control )

Considere, digamos, este pseudocódigo hipotético:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyaquí está resolviendo completamente un problema de control y no un problema de gestión de recursos.
No tendría sentido hacer eso en un destructor por una variedad de razones:

  • No lo está siendo "adquirida" o "creado"
  • Si no se imprime en el archivo de registro no se producirán fugas de recursos, corrupción de datos, etc. (suponiendo que el archivo de registro aquí no se envíe al programa en otro lugar)
  • Es legítimo logfile.printfracasar, mientras que la destrucción (conceptual) no puede fallar

Aquí hay otro ejemplo, esta vez como en Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

En el ejemplo anterior, nuevamente, no hay recursos para liberar.
De hecho, el finallybloque está adquiriendo recursos internamente para lograr su objetivo, lo que podría fallar. Por lo tanto, no tiene sentido usar un destructor (si Javascript tenía uno).

Por otro lado, en este ejemplo:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyestá destruyendo un recurso, b. Es un problema de datos. El problema no se trata de devolver limpiamente el control a la persona que llama, sino de evitar pérdidas de recursos.
El fracaso no es una opción, y nunca (conceptualmente) debería ocurrir.
Cada lanzamiento de bnecesariamente está emparejado con una adquisición, y tiene sentido usar RAII.

En otras palabras, solo porque puede usar cualquiera para simular eso no significa que ambos son el mismo problema o que ambos son soluciones apropiadas para ambos problemas.

usuario541686
fuente
Gracias. No estoy de acuerdo, pero oye :-) Creo que podré agregar una respuesta de opinión opuesta exhaustiva en los próximos días ...
Martin Ba
2
¿Cómo influye en esto el hecho de que finallyse usa principalmente para liberar recursos (sin memoria)?
Bart van Ingen Schenau
1
@BartvanIngenSchenau: Nunca discutí que ningún lenguaje actualmente en existencia tenga una filosofía o implementación que coincida con lo que describí. La gente todavía no ha terminado de inventar todo lo que podría existir. Solo sostuve que sería valioso separar las dos nociones ya que son ideas diferentes y tienen diferentes casos de uso. Para satisfacer tu curiosidad, creo que D tiene ambas. Probablemente también hay otros idiomas. Sin embargo, no lo considero relevante, y no podría importarme menos por qué, por ejemplo, Java estaba a favor finally.
user541686
1
Un ejemplo práctico que he encontrado en JavaScript son funciones que cambian temporalmente el puntero del mouse a un reloj de arena durante una operación larga (lo que podría generar una excepción), y luego lo vuelven a normalizar en la finallycláusula. La cosmovisión de C ++ introduciría una clase que administra este "recurso" de una asignación a una variable pseudo-global. ¿Qué sentido conceptual tiene eso? Pero los destructores son el martillo de C ++ para la ejecución requerida del código de fin de bloque.
dan04
1
@ dan04: Muchas gracias, ese es el ejemplo perfecto para esto. Podría jurar que me encontré con tantas situaciones en las que RAII no tenía sentido, pero me costó mucho pensar en ellas.
user541686
1

La respuesta de k3b realmente lo expresa muy bien:

un destructor en c ++ es un mecanismo implícito (invocado automáticamente) para liberar recursos asignados mientras que el intento ... finalmente puede usarse como un mecanismo explícito para hacerlo.

En cuanto a "recursos", me gusta referirme a Jon Kalb: RAII debería significar Adquisición de responsabilidad es inicialización .

De todos modos, en cuanto a lo implícito frente a lo explícito, esto parece ser realmente:

  • Un d'tor es una herramienta para definir qué operaciones sucederán, implícitamente, cuando finaliza la vida útil de un objeto (que a menudo coincide con el final del alcance)
  • Un bloque final es una herramienta para definir, explícitamente, qué operaciones sucederán al final del alcance.
  • Además, técnicamente, siempre se te permite lanzar finalmente, pero mira a continuación.

Creo que eso es todo por la parte conceptual, ...


... ahora hay en mi humilde opinión algunos detalles interesantes:

Tampoco creo que c'tor / d'tor necesite conceptualmente "adquirir" o "crear" nada, aparte de la responsabilidad de ejecutar algún código en el destructor. Que es lo que finalmente hace también: ejecuta un código.

Y aunque el código en un bloque finalmente puede arrojar una excepción, no es suficiente distinción para mí para decir que son conceptualmente diferentes por encima de lo explícito frente a lo implícito.

(Además, no estoy convencido de que ese código "bueno" deba arrojarse finalmente, tal vez esa sea otra pregunta completa para sí mismo).

Martin Ba
fuente
¿Qué piensas sobre mi ejemplo de Javascript?
user541686
Con respecto a sus otros argumentos: "¿Realmente queremos registrar la misma cosa independientemente?" Sí, es solo un ejemplo y te estás perdiendo el punto, y sí, a nadie se le ha prohibido registrar detalles más específicos para cada caso. El punto aquí es que ciertamente no puede afirmar que nunca hay una situación en la que desee registrar algo que sea común para ambos. Algunas entradas de registro son genéricas, algunas son específicas; quieres los dos. Y de nuevo, estás perdiendo el punto por completo al enfocarte en el registro. Motivar ejemplos de 10 líneas es difícil; Intenta no perderte el punto.
user541686
Nunca abordó estos ...
user541686
@Mehrdad: no abordé su ejemplo de JavaScript porque me llevaría otra página para analizar lo que pienso de él. (Lo intenté, pero me tomó tanto tiempo, decir algo coherente que lo salté :-)
Martin Ba
@Mehrdad, en cuanto a sus otros puntos, parece que tenemos que estar de acuerdo en no estar de acuerdo. Veo a dónde apunta con la diferencia, pero simplemente no estoy convencido de que sean algo conceptualmente diferente: principalmente porque estoy principalmente en el campamento que piensa que tirar finalmente es una muy mala idea ( nota : también piense en su observerejemplo que lanzar allí sería una muy mala idea). Siéntase libre de abrir un chat, si desea discutir esto más a fondo. Ciertamente fue divertido pensar en tus argumentos. Salud.
Martin Ba