¿Por qué no hay una construcción 'finalmente' en C ++?

57

El manejo de excepciones en C ++ se limita a try / throw / catch. A diferencia de Object Pascal, Java, C # y Python, incluso en C ++ 11, la finallyconstrucción no se ha implementado.

He visto una gran cantidad de literatura de C ++ sobre "código seguro de excepción". Lippman escribe que el código seguro de excepción es un tema importante pero avanzado y difícil, más allá del alcance de su Manual, lo que parece implicar que el código seguro no es fundamental para C ++. ¡Herb Sutter dedica 10 capítulos al tema en su Excepcional C ++!

Sin embargo, me parece que muchos de los problemas encontrados al intentar escribir "código seguro de excepción" podrían resolverse bastante bien si finallyse implementara la construcción, lo que le permite al programador asegurarse de que incluso en caso de una excepción, el programa pueda restaurarse a un estado seguro, estable y sin fugas, cerca del punto de asignación de recursos y código potencialmente problemático. Como programador muy experimentado de Delphi y C #, uso try .. finalmente bloquea bastante en mi código, al igual que la mayoría de los programadores en estos lenguajes.

Teniendo en cuenta todas las 'campanas y silbatos' implementados en C ++ 11, me sorprendió descubrir que 'finalmente' todavía no estaba allí.

Entonces, ¿por qué la finallyconstrucción nunca se ha implementado en C ++? Realmente no es un concepto muy difícil o avanzado de comprender y ayuda mucho al programador a escribir 'código seguro de excepción'.

Vector
fuente
25
¿Por qué no finalmente? Porque liberas cosas en el destructor que se dispara automáticamente cuando el objeto (o puntero inteligente) deja el alcance. Los destructores son superiores a finalmente {} ya que separa el flujo de trabajo de la lógica de limpieza. Del mismo modo que no desea que las llamadas a free () desordenen su flujo de trabajo en un idioma recolectado como basura.
mike30
2
Consulte también ¿Los desarrolladores de Java abandonaron conscientemente RAII?
BlueRaja - Danny Pflughoeft
8
Haciendo la pregunta, "¿Por qué no hay finallyen C ++, y qué técnicas para el manejo de excepciones se utilizan en su lugar?" es válido y está en el tema de este sitio. Las respuestas existentes cubren esto bien, creo. Convirtiéndolo en una discusión sobre "¿Son finallyvaliosas las razones de los diseñadores de C ++ para no incluirlas ?" y "¿Debería finallyagregarse a C ++?" y continuar la discusión a través de los comentarios sobre la pregunta y cada respuesta no se ajusta al modelo de este sitio de preguntas y respuestas.
Josh Kelley
2
Si finalmente lo ha hecho, ya tiene una separación de preocupaciones: el bloque de código principal está aquí, y la preocupación de limpieza se soluciona aquí.
Kaz
2
@Kaz. La diferencia es la limpieza implícita vs explícita. Un destructor le brinda una limpieza automática similar a la forma en que se limpia una primitiva simple y antigua a medida que emerge de la pila. No necesita hacer ninguna llamada de limpieza explícita y puede centrarse en su lógica central. Imagine lo complicado que sería si tuviera que limpiar las primitivas asignadas de la pila en un intento / finalmente. La limpieza implícita es superior. La comparación de la sintaxis de clase con las funciones anónimas no es relevante. Aunque al pasar funciones de primera clase a una función que libera un identificador podría centralizar la limpieza manual.
mike30

Respuestas:

57

Como comentario adicional sobre la respuesta de @ Nemanja (que, dado que cita a Stroustrup, es realmente una respuesta tan buena como puedes obtener):

Realmente es solo una cuestión de entender la filosofía y los modismos de C ++. Tome su ejemplo de una operación que abre una conexión de base de datos en una clase persistente y tiene que asegurarse de que cierra esa conexión si se produce una excepción. Esta es una cuestión de seguridad excepcional y se aplica a cualquier lenguaje con excepciones (C ++, C #, Delphi ...).

En un lenguaje que usa try/ finally, el código podría verse así:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Simple y directo. Sin embargo, hay algunas desventajas:

  • Si el lenguaje no tiene destructores deterministas, siempre tengo que escribir el finallybloque, de lo contrario pierdo recursos.
  • Si se DoRiskyOperationtrata de más de una llamada a un método, si tengo que procesar algo en el trybloque, entonces la Closeoperación puede terminar estando un poco lejos de la Openoperación. No puedo escribir mi limpieza justo al lado de mi adquisición.
  • Si tengo varios recursos que necesitan ser adquiridos y luego liberados de manera segura, puedo terminar con varias capas de profundidad try/ finallybloques.

El enfoque de C ++ se vería así:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

Esto resuelve completamente todas las desventajas del finallyenfoque. Tiene algunas desventajas propias, pero son relativamente menores:

  • Hay una buena posibilidad de que necesites escribir la ScopedDatabaseConnectionclase tú mismo. Sin embargo, es una implementación muy simple: solo 4 o 5 líneas de código.
  • Implica crear una variable local adicional, de la que aparentemente no eres fanático, basado en tu comentario sobre "crear y destruir constantemente clases para confiar en sus destructores para limpiar tu desorden es muy pobre", pero un buen compilador optimizará eliminar el trabajo adicional que implica una variable local adicional. El buen diseño de C ++ se basa mucho en este tipo de optimizaciones.

Personalmente, considerando estas ventajas y desventajas, considero que RAII es una técnica mucho más preferible finally. Su experiencia puede ser diferente.

Finalmente, debido a que RAII es un idioma muy bien establecido en C ++, y para aliviar a los desarrolladores de la carga de escribir numerosas Scoped...clases, hay bibliotecas como ScopeGuard y Boost.ScopeExit que facilitan este tipo de limpieza determinista.

Josh Kelley
fuente
8
C # tiene la usingdeclaración, que limpia automáticamente cualquier objeto que implemente la IDisposableinterfaz. Entonces, aunque es posible equivocarse, es bastante fácil hacerlo bien.
Robert Harvey
18
Tener que escribir una clase completamente nueva para encargarse de la reversión temporal del cambio de estado, utilizando un modismo de diseño implementado por el compilador con una try/finallyconstrucción porque el compilador no expone una try/finallyconstrucción y la única forma de acceder a ella es a través de la clase diseño idiomático, no es una "ventaja"; Es la definición misma de una inversión de abstracción.
Mason Wheeler
15
@MasonWheeler - Umm, no dije que tener que escribir una nueva clase es una ventaja. Dije que es una desventaja. En general, sin embargo, prefiero RAII a tener que usar finally. Como dije, su kilometraje puede variar.
Josh Kelley
77
@JoshKelley: "Un buen diseño de C ++ depende mucho de este tipo de optimizaciones". ¿Escribir buenos trozos de código extraño y luego confiar en la optimización del compilador es un buen diseño ? OMI es la antítesis del buen diseño. Entre los fundamentos del buen diseño se encuentra el código conciso y fácil de leer. Menos para depurar, menos para mantener, etc., etc. NO debería escribir fragmentos de código y luego confiar en el compilador para que todo desaparezca: ¡OMI, eso no tiene ningún sentido!
Vector
14
@Mikey: ¿Entonces duplicar el código de limpieza (o el hecho de que la limpieza debe realizarse) en todo el código base es "conciso" y "fácilmente legible"? Con RAII, usted escribe dicho código una vez, y se aplica automáticamente en todas partes.
Mankarse
55

¿ Por qué C ++ no proporciona una construcción "finalmente"? en Preguntas frecuentes sobre el estilo y la técnica C ++ de Bjarne Stroustrup :

Debido a que C ++ admite una alternativa que casi siempre es mejor: la técnica de "adquisición de recursos es inicialización" (TC ++ PL3 sección 14.4). La idea básica es representar un recurso por un objeto local, de modo que el destructor del objeto local libere el recurso. De esa manera, el programador no puede olvidar liberar el recurso.

Nemanja Trifunovic
fuente
55
Pero no hay nada sobre esa técnica que sea específica de C ++, ¿verdad? Puede hacer RAII en cualquier lenguaje con objetos, constructores y destructores. Es una gran técnica, pero RAII simplemente existente no implica que una finallyconstrucción siempre sea inútil para siempre, a pesar de lo que Strousup está diciendo. El simple hecho de que escribir "código seguro de excepción" es un gran problema en C ++ es prueba de ello. Heck, C # tiene dos destructores y finally, y que tanto se acostumbre.
Tacroy
28
@Tacroy: C ++ es uno de los pocos lenguajes principales que tiene destructores deterministas . Los "destructores" de C # son inútiles para este propósito, y usted necesita escribir manualmente "usando" bloques para tener RAII.
Nemanja Trifunovic
15
@ Mike tiene la respuesta de "¿Por qué C ++ no proporciona una construcción" finalmente "? directamente del propio Stroustrup allí. ¿Qué más puedes pedir? Eso es por qué.
55
@Mikey Si le preocupa que su código se comporte bien, en particular que no pierda recursos, cuando se le lanzan excepciones, está preocupado por la seguridad de excepciones / tratando de escribir un código seguro de excepción. Simplemente no lo está llamando así, y debido a las diferentes herramientas disponibles, lo implementa de manera diferente. Pero es exactamente de lo que habla la gente de C ++ cuando discuten la seguridad de las excepciones.
19
@Kaz: Solo necesito recordar hacer la limpieza en el destructor una vez, y desde entonces solo uso el objeto. Necesito recordar hacer la limpieza en el bloque finalmente cada vez que uso la operación que asigna.
deworde
19

La razón por la que C ++ no tiene finallyes porque no se necesita en C ++. finallyse usa para ejecutar algún código independientemente de si se produjo una excepción o no, lo que casi siempre es algún tipo de código de limpieza. En C ++, este código de limpieza debe estar en el destructor de la clase relevante y siempre se llamará al destructor, como un finallybloque. El idioma de usar el destructor para su limpieza se llama RAII .

Dentro de la comunidad C ++ puede que se hable más sobre el código de "excepción segura", pero es casi igualmente importante en otros lenguajes que tienen excepciones. El punto principal del código de 'excepción segura' es que usted piensa en qué estado queda su código si ocurre una excepción en cualquiera de las funciones / métodos que llama.
En C ++, el código de 'excepción segura' es un poco más importante, porque C ++ no tiene recolección automática de basura que se encarga de los objetos que quedan huérfanos debido a una excepción.

La razón por la cual la seguridad de excepción se discute más en la comunidad de C ++ probablemente también se debe al hecho de que en C ++ debe ser más consciente de lo que puede salir mal, porque hay menos redes de seguridad predeterminadas en el lenguaje.

Bart van Ingen Schenau
fuente
2
Nota: Por favor, no sostenga que C ++ tiene destructores deterministas. Object Pascal / Delphi también tiene destructores deterministas pero también admite 'finalmente', por las muy buenas razones que expliqué en mis primeros comentarios a continuación.
Vector
13
@Mikey: Dado que nunca ha habido una propuesta para agregar finallyal estándar C ++, creo que es seguro concluir que la comunidad C ++ no considera the absence of finallyun problema. La mayoría de los lenguajes que finallycarecen de la destrucción determinista consistente que tiene C ++. Veo que Delphi los tiene a ambos, pero no conozco su historia lo suficientemente bien como para saber cuál estuvo allí primero.
Bart van Ingen Schenau
3
Dephi no admite objetos basados ​​en pila, solo referencias basadas en montón y objetos en la pila. Por lo tanto, 'finalmente' es necesario para invocar explícitamente destructores, etc. cuando sea apropiado.
Vector
2
Hay una gran cantidad de problemas en C ++ que posiblemente no sean necesarios, por lo que esta no puede ser la respuesta correcta.
Kaz
15
En las más de dos décadas que usé el lenguaje y trabajé con otras personas que lo usaron, nunca me encontré con un programador de C ++ que dijera "Realmente desearía que el lenguaje tuviera finally". Nunca recuerdo ninguna tarea que hubiera facilitado si hubiera tenido acceso a ella.
Gort the Robot
12

Otros han discutido RAII como la solución. Es una solución perfectamente buena. Pero eso realmente no aborda por qué no agregaron finallytambién, ya que es algo muy deseado. La respuesta a eso es más fundamental para el diseño y desarrollo de C ++: durante el desarrollo de C ++, los involucrados se han resistido fuertemente a la introducción de características de diseño que se pueden lograr utilizando otras características sin una gran cantidad de complicaciones y especialmente cuando esto requiere la introducción de nuevas palabras clave que podrían hacer que el código anterior sea incompatible. Dado que RAII proporciona una alternativa altamente funcional finallyy finally, de todos modos, puede implementar la suya propia en C ++ 11, hubo poca necesidad de hacerlo.

Todo lo que necesita hacer es crear una clase Finallyque llame a la función pasada a su constructor en su destructor. Entonces puedes hacer esto:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

Sin embargo, la mayoría de los programadores nativos de C ++ preferirán, en general, objetos RAII de diseño limpio.

Jack Aidley
fuente
3
Te estás perdiendo la captura de referencia en tu lambda. Debería ser Finally atEnd([&] () { database.close(); });también, me imagino que lo siguiente es mejor: { Finally atEnd(...); try {...} catch(e) {...} }(saqué el finalizador del bloque de prueba para que se ejecute después de los bloques de captura).
Thomas Eding
2

Puede usar un patrón de "trampa", incluso si no desea usar el bloque try / catch.

Poner un objeto simple en el alcance requerido. En el destructor de este objeto pon tu lógica "final". No importa qué, cuando la pila se desenrolla, se llamará al destructor del objeto y obtendrás tus dulces.

Arie R
fuente
1
Esto no responde a la pregunta, y simplemente demuestra que , finalmente, no es una mala idea después de todo ...
Vector
2

Bueno, podría hacer una especie de roll-own finally, usando Lambdas, que obtendría lo siguiente para compilar bien (usando un ejemplo sin RAII, por supuesto, no es el mejor código):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Ver este artículo .

einpoklum - reinstalar a Monica
fuente
-2

No estoy seguro de estar de acuerdo con las afirmaciones aquí de que RAII es un superconjunto finally. El talón de Aquiles de RAII es simple: excepciones. RAII se implementa con destructores, y siempre está mal en C ++ tirar de un destructor. Eso significa que no puede usar RAII cuando necesita su código de limpieza para lanzar. Si finallyse implementaran, por otro lado, no hay razón para creer que no sería legal lanzar desde un finallybloque.

Considere una ruta de código como esta:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Si tuviéramos finally, podríamos escribir:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Pero no hay manera, que pueda encontrar, de obtener el comportamiento equivalente usando RAII.

Si alguien sabe cómo hacer esto en C ++, estoy muy interesado en la respuesta. Incluso estaría contento con algo que se basara, por ejemplo, en hacer cumplir todas las excepciones heredadas de una sola clase con algunas capacidades especiales o algo así.

Científico loco
fuente
1
En su segundo ejemplo, si complex_cleanuppuede lanzar, entonces podría tener un caso en el que dos excepciones no detectadas están en vuelo al mismo tiempo, tal como lo haría con RAII / destructores, y C ++ se niega a permitir esto. Si desea que se vea la excepción original, complex_cleanupdebe evitar cualquier excepción, tal como lo haría con RAII / destructores. Si desea complex_cleanupque se vea la excepción, entonces creo que puede usar bloques try / catch anidados, aunque esta es una tangente y difícil de encajar en un comentario, por lo que vale la pena hacer una pregunta por separado.
Josh Kelley el
Quiero usar RAII para obtener un comportamiento idéntico como el primer ejemplo, de manera más segura. Un lanzamiento en un finallybloque supuesto funcionaría claramente igual que un lanzamiento en un catchbloque WRT, excepto en vuelo, no en llamadas std::terminate. La pregunta es "¿por qué no finallyen C ++?" y todas las respuestas dicen "no lo necesitas ... RAII FTW!" Mi punto es que sí, RAII está bien para casos simples como la administración de memoria, pero hasta que se resuelva el problema de las excepciones, se requiere demasiado pensamiento / sobrecarga / preocupación / rediseño para ser una solución de propósito general.
MadScientist
3
Entiendo su punto, hay algunos problemas legítimos con los destructores que podrían arrojar, pero son raros. Decir que las excepciones RAII + tienen problemas no resueltos o que RAII no es una solución de propósito general simplemente no coincide con la experiencia de la mayoría de los desarrolladores de C ++.
Josh Kelley
1
Si se encuentra con la necesidad de generar excepciones en los destructores, está haciendo algo mal, probablemente utilizando punteros en otros lugares cuando no son necesarios.
Vector
1
Esto es demasiado complicado para comentarios. Publique una pregunta al respecto: ¿Cómo manejaría este escenario en C ++ usando el modelo RAII ... no parece funcionar ... Nuevamente, debe dirigir sus comentarios : escriba @ y el nombre del miembro del que está hablando al principio de tu comentario. Cuando los comentarios están en su propia publicación, recibe una notificación de todo, pero otros no, a menos que les envíe un comentario.
Vector