programación - ¿Soporta C++ los bloques ''finalmente''?(¿Y sobre qué es este ''RAII'' del que escucho?)
sentencias basicas de programacion (15)
¿por qué es que incluso los lenguajes administrados proporcionan un bloque final a pesar de que los recursos son asignados automáticamente por el recolector de basura de todos modos?
En realidad, los idiomas basados en recolectores de basura necesitan "finalmente" más. Un recolector de basura no destruye sus objetos de manera oportuna, por lo que no se puede confiar en que limpie los problemas no relacionados con la memoria correctamente.
En términos de datos asignados dinámicamente, muchos argumentarían que debería usar punteros inteligentes.
Sin embargo...
RAII transfiere la responsabilidad de la seguridad de excepción del usuario del objeto al diseñador
Lamentablemente esta es su propia caída. Los viejos hábitos de programación C mueren duro. Cuando está utilizando una biblioteca escrita en C o en un estilo muy C, no se habrá utilizado RAII. Aparte de volver a escribir todo el front-end de la API, eso es justo con lo que tienes que trabajar. Entonces la falta de "finalmente" realmente muerde.
¿Soporta C ++ los bloques '' finally ''?
¿Cuál es el lenguaje RAII ?
¿Cuál es la diferencia entre el lenguaje RAII de C ++ y la declaración ''using'' de C # ?
Además de facilitar la limpieza con objetos basados en la pila, RAII también es útil porque la misma limpieza "automática" se produce cuando el objeto es miembro de otra clase. Cuando se destruye la clase propietaria, el recurso administrado por la clase RAII se limpia porque como resultado se llama al dtor de esa clase.
Esto significa que cuando llega a nirvana RAII y todos los miembros de una clase usan RAII (como punteros inteligentes), puede salirse con un dtor muy simple (tal vez incluso predeterminado) para la clase de propietario, ya que no necesita administrar manualmente su Recursos de vida de los miembros.
Como muchas personas han dicho, la solución es usar las características de C ++ 11 para evitar finalmente los bloques. Una de las características es unique_ptr
.
Aquí está la respuesta de Mephane escrita usando patrones RAII.
#include <vector>
#include <memory>
#include <list>
using namespace std;
class Foo
{
...
};
void DoStuff(vector<string> input)
{
list<unique_ptr<Foo> > myList;
for (int i = 0; i < input.size(); ++i)
{
myList.push_back(unique_ptr<Foo>(new Foo(input[i])));
}
DoSomeStuff(myList);
}
Aquí hay más introducción al uso de unique_ptr con los contenedores de la biblioteca estándar de C ++.
Como se señaló en las otras respuestas, C ++ puede admitir finally
funcionalidad similar. La implementación de esta funcionalidad que probablemente esté más cerca de ser parte del lenguaje estándar es la que acompaña a las Pautas principales de C ++ , un conjunto de mejores prácticas para usar C ++ editado por Bjarne Stoustrup y Herb Sutter. Una implementación de finally
es parte de la Biblioteca de soporte de directrices (GSL). A lo largo de las Pautas, se recomienda el uso de finally
al tratar con interfaces de estilo antiguo, y también tiene una guía propia, titulada Usar un objeto final_action para expresar la limpieza si no hay disponible un controlador de recursos adecuado .
Por lo tanto, no solo es compatible con C ++ finally
, se recomienda usarlo en muchos casos de uso comunes.
Un ejemplo de uso de la implementación de GSL sería:
#include <gsl/gsl_util.h>
void example()
{
int handle = get_some_resource();
auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });
// Do a lot of stuff, return early and throw exceptions.
// clean_that_resource will always get called.
}
La implementación y uso de GSL es muy similar al de la respuesta de Paolo.Bolzoni . Una diferencia es que el objeto creado por gsl::finally()
carece de la llamada disable()
. Si necesita esa funcionalidad (por ejemplo, para devolver el recurso una vez que esté ensamblado y no se produzcan excepciones), es posible que prefiera la implementación de Paolo. De lo contrario, usar GSL es lo más parecido a usar características estandarizadas que obtendrá.
En C ++ 11, si es necesario, RAII permite hacer un finalmente:
namespace detail { //adapt to your "private" namespace
template <typename F>
struct FinalAction {
FinalAction(F f) : clean_{f} {}
~FinalAction() { if(enabled_) clean_(); }
void disable() { enabled_ = false; };
private:
F clean_;
bool enabled_{true}; }; }
template <typename F>
detail::FinalAction<F> finally(F f) {
return detail::FinalAction<F>(f); }
ejemplo de uso:
#include <iostream>
int main() {
int* a = new int;
auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!/n"; });
std::cout << "doing something .../n"; }
La salida será:
doing something...
leaving the block, deleting a!
Personalmente, lo utilicé varias veces para asegurarme de cerrar el descriptor de archivo POSIX en un programa C ++.
Tener una clase real que administre recursos y, por lo tanto, evite cualquier tipo de fugas suele ser mejor, pero finalmente es útil en los casos en que hacer una clase suene como una exageración.
Además, finalmente me gusta más que otros idiomas porque, si se usa de forma natural, escribe el código de cierre cerca del código de apertura (en mi ejemplo, el nuevo y el borrado ) y la destrucción sigue la construcción en el orden LIFO como es habitual en C ++. El único inconveniente es que obtienes una variable automática que realmente no usas y la sintaxis lambda la hace un poco ruidosa (en mi ejemplo en la cuarta línea, solo la palabra finalmente y el bloque {} a la derecha son significativos, el el resto es esencialmente ruido).
Otro ejemplo:
[...]
auto precision = std::cout.precision();
auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } );
std::cout << std::setprecision(3);
El miembro deshabilitado es útil si el último tiene que ser llamado solo en caso de falla. Por ejemplo, tiene que copiar un objeto en tres contenedores diferentes, puede configurar el último para deshacer cada copia y deshabilitarla después de que todas las copias sean exitosas. Si lo hace, si la destrucción no puede tirar, se asegura la fuerte garantía.
deshabilitar ejemplo:
//strong guarantee
void copy_to_all(BIGobj const& a) {
first_.push_back(a);
auto undo_first_push = finally([first_&] { first_.pop_back(); });
second_.push_back(a);
auto undo_second_push = finally([second_&] { second_.pop_back(); });
third_.push_back(a);
//no necessary, put just to make easier to add containers in the future
auto undo_third_push = finally([third_&] { third_.pop_back(); });
undo_first_push.disable();
undo_second_push.disable();
undo_third_push.disable(); }
En C ++, finalmente NO se requiere debido a RAII.
RAII transfiere la responsabilidad de excepción de seguridad del usuario del objeto al diseñador (y al implementador) del objeto. Yo diría que este es el lugar correcto, ya que solo necesita una excepción de seguridad correcta una vez (en el diseño / implementación). Al usar finalmente, necesita obtener la seguridad de excepción correcta cada vez que use un objeto.
También IMO el código parece más limpio (ver más abajo).
Ejemplo:
Un objeto de base de datos. Para asegurarse de que se utiliza la conexión DB, debe abrirse y cerrarse. Al usar RAII, esto se puede hacer en el constructor / destructor.
C ++ como RAII
void someFunc()
{
DB db("DBDesciptionString");
// Use the db object.
} // db goes out of scope and destructor closes the connection.
// This happens even in the presence of exceptions.
El uso de RAII hace que el uso correcto de un objeto DB sea muy fácil. El objeto DB se cerrará correctamente mediante el uso de un destructor, sin importar cómo intentemos abusarlo.
Java Like Finalmente
void someFunc()
{
DB db = new DB("DBDesciptionString");
try
{
// Use the db object.
}
finally
{
// Can not rely on finaliser.
// So we must explicitly close the connection.
try
{
db.close();
}
catch(Throwable e)
{
/* Ignore */
// Make sure not to throw exception if one is already propagating.
}
}
}
Cuando se usa finalmente, el uso correcto del objeto se delega al usuario del objeto. es decir , es responsabilidad del usuario del objeto cerrar correctamente la conexión DB. Ahora puede argumentar que esto se puede hacer en el finalizador, pero los recursos pueden tener una disponibilidad limitada u otras restricciones y, por lo tanto, generalmente desea controlar la liberación del objeto y no confiar en el comportamiento no determinista del recolector de basura.
También este es un ejemplo simple.
Cuando tiene múltiples recursos que necesitan ser liberados, el código puede complicarse.
Un análisis más detallado se puede encontrar aquí: http://accu.org/index.php/journals/236
En realidad no, pero puedes emularlos hasta cierto punto, por ejemplo:
int * array = new int[10000000];
try {
// Some code that can throw exceptions
// ...
throw std::exception();
// ...
} catch (...) {
// The finally-block (if an exception is thrown)
delete[] array;
// re-throw the exception.
throw;
}
// The finally-block (if no exception was thrown)
delete[] array;
Tenga en cuenta que el bloque finally podría lanzar una excepción antes de que se vuelva a lanzar la excepción original, descartando así la excepción original. Este es exactamente el mismo comportamiento que en un bloque finalmente de Java. Además, no puedes usar return
dentro de los bloques try & catch.
FWIW, Microsoft Visual C ++ admite el intento, finalmente, e históricamente se ha utilizado en aplicaciones MFC como un método para detectar excepciones graves que de otro modo darían como resultado un bloqueo. Por ejemplo;
int CMyApp::Run()
{
__try
{
int i = CWinApp::Run();
m_Exitok = MAGIC_EXIT_NO;
return i;
}
__finally
{
if (m_Exitok != MAGIC_EXIT_NO)
FaultHandler();
}
}
He usado esto en el pasado para hacer cosas como guardar copias de seguridad de archivos abiertos antes de salir. Sin embargo, ciertas configuraciones de depuración de JIT romperán este mecanismo.
Lo siento por desenterrar un hilo tan viejo, pero hay un error importante en el siguiente razonamiento:
RAII transfiere la responsabilidad de excepción de seguridad del usuario del objeto al diseñador (y al implementador) del objeto. Yo diría que este es el lugar correcto, ya que solo necesita una excepción de seguridad correcta una vez (en el diseño / implementación). Al usar finalmente, necesita obtener la seguridad de excepción correcta cada vez que use un objeto.
La mayoría de las veces, tiene que lidiar con objetos asignados dinámicamente, números dinámicos de objetos, etc. Dentro del bloque try, algunos códigos pueden crear muchos objetos (cuántos se determinan en tiempo de ejecución) y almacenar los punteros en una lista. Ahora, este no es un escenario exótico, pero muy común. En este caso, querrías escribir cosas como
void DoStuff(vector<string> input)
{
list<Foo*> myList;
try
{
for (int i = 0; i < input.size(); ++i)
{
Foo* tmp = new Foo(input[i]);
if (!tmp)
throw;
myList.push_back(tmp);
}
DoSomeStuff(myList);
}
finally
{
while (!myList.empty())
{
delete myList.back();
myList.pop_back();
}
}
}
Por supuesto, la lista en sí misma se destruirá cuando salga del alcance, pero eso no limpiaría los objetos temporales que ha creado.
En su lugar, tienes que ir por el camino feo:
void DoStuff(vector<string> input)
{
list<Foo*> myList;
try
{
for (int i = 0; i < input.size(); ++i)
{
Foo* tmp = new Foo(input[i]);
if (!tmp)
throw;
myList.push_back(tmp);
}
DoSomeStuff(myList);
}
catch(...)
{
}
while (!myList.empty())
{
delete myList.back();
myList.pop_back();
}
}
Además: ¿por qué es que incluso los idiomas administrados proporcionan un bloque final a pesar de que los recursos son desasignados automáticamente por el recolector de basura de todos modos?
Sugerencia: hay más que puede hacer con "finalmente" que solo la desasignación de memoria.
Me gustaría ofrecer una alternativa.
Si desea que el bloque final se llame siempre, solo colóquelo después del último bloque catch (que probablemente debería ser catch( ... )
para capturar la excepción no conocida)
try{
// something that might throw exception
} catch( ... ){
// what to do with uknown exception
}
//final code to be called always,
//don''t forget that it might throw some exception too
doSomeCleanUp();
Si finalmente desea bloquear como última cosa cuando se lanza una excepción, puede usar una variable local booleana. Antes de ejecutarla, establezca el valor en falso y coloque la asignación verdadera al final del bloque try, luego, después de la captura del bloque catch, busque la variable. valor:
bool generalAppState = false;
try{
// something that might throw exception
//the very end of try block:
generalAppState = true;
} catch( ... ){
// what to do with uknown exception
}
//final code to be called only when exception was thrown,
//don''t forget that it might throw some exception too
if( !generalAppState ){
doSomeCleanUpOfDirtyEnd();
}
//final code to be called only when no exception is thrown
//don''t forget that it might throw some exception too
else{
cleanEnd();
}
No, C ++ no soporta bloques ''finalmente''. La razón es que C ++, en cambio, es compatible con RAII: "La adquisición de recursos es la inicialización", un mal nombre † para un concepto realmente útil.
La idea es que el destructor de un objeto es responsable de liberar recursos. Cuando el objeto tiene una duración de almacenamiento automático, se llamará al destructor del objeto cuando salga el bloque en el que se creó, incluso cuando ese bloque se salga en presencia de una excepción. Aquí está la explicación de Bjarne Stroustrup del tema.
Un uso común para RAII es bloquear un mutex:
// A class with implements RAII
class lock
{
mutex &m_;
public:
lock(mutex &m)
: m_(m)
{
m.acquire();
}
~lock()
{
m_.release();
}
};
// A class which uses ''mutex'' and ''lock'' objects
class foo
{
mutex mutex_; // mutex for locking ''foo'' object
public:
void bar()
{
lock scopeLock(mutex_); // lock object.
foobar(); // an operation which may throw an exception
// scopeLock will be destructed even if an exception
// occurs, which will release the mutex and allow
// other functions to lock the object and run.
}
};
RAII también simplifica el uso de objetos como miembros de otras clases. Cuando la clase propietaria se destruye, el recurso administrado por la clase RAII se libera porque, como resultado, se llama al destructor para la clase administrada por RAII. Esto significa que cuando usa RAII para todos los miembros de una clase que administran recursos, puede utilizar un destructor muy simple, tal vez incluso el predeterminado, para la clase de propietario, ya que no es necesario que administre manualmente la vida útil de los recursos de los miembros. . (Gracias a Mike B por señalar esto).
Para aquellos familiares con C # o VB.NET, puede reconocer que RAII es similar a la destrucción determinista de .NET usando IDisposable y declaraciones de "uso" . De hecho, los dos métodos son muy similares. La principal diferencia es que RAII lanzará de forma determinista cualquier tipo de recurso, incluida la memoria. Al implementar IDisposable en .NET (incluso el lenguaje .NET C ++ / CLI), los recursos se liberarán de manera determinista, excepto la memoria. En .NET, la memoria no se libera de manera determinista; La memoria solo se libera durante los ciclos de recolección de basura.
† Algunas personas creen que "La destrucción es la renuncia a los recursos" es un nombre más preciso para el lenguaje RAII.
Otra emulación de bloque "finalmente" utilizando las funciones lambda de C ++ 11
template <typename TCode, typename TFinallyCode>
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
try
{
code();
}
catch (...)
{
try
{
finally_code();
}
catch (...) // Maybe stupid check that finally_code mustn''t throw.
{
std::terminate();
}
throw;
}
finally_code();
}
Esperemos que el compilador optimice el código de arriba.
Ahora podemos escribir código como este:
with_finally(
[&]()
{
try
{
// Doing some stuff that may throw an exception
}
catch (const exception1 &)
{
// Handling first class of exceptions
}
catch (const exception2 &)
{
// Handling another class of exceptions
}
// Some classes of exceptions can be still unhandled
},
[&]() // finally
{
// This code will be executed in all three cases:
// 1) exception was not thrown at all
// 2) exception was handled by one of the "catch" blocks above
// 3) exception was not handled by any of the "catch" block above
}
);
Si lo deseas, puedes envolver este idioma en macros de "prueba - finalmente":
// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};
#define begin_try with_finally([&](){ try
#define finally catch(never_thrown_exception){throw;} },[&]()
#define end_try ) // sorry for "pascalish" style :(
Ahora el bloque "finalmente" está disponible en C ++ 11:
begin_try
{
// A code that may throw
}
catch (const some_exception &)
{
// Handling some exceptions
}
finally
{
// A code that is always executed
}
end_try; // Sorry again for this ugly thing
Personalmente, no me gusta la versión "macro" del lenguaje "finalmente" y preferiría utilizar la función pura "with_finally" aunque la sintaxis sea más voluminosa en ese caso.
Puede probar el código anterior aquí: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813
Se me ocurrió una macro finally
que se puede usar casi como keyword la palabra clave finally
en Java; hace uso de std::exception_ptr
y amigos, funciones lambda y std::promise
, por lo que requiere C++11
o superior; también hace uso de la extensión de la expresión GCC de la instrucción compuesta , que también es compatible con clang.
ADVERTENCIA : una versión anterior de esta respuesta utilizó una implementación diferente del concepto con muchas más limitaciones.
Primero, definamos una clase auxiliar.
#include <future>
template <typename Fun>
class FinallyHelper {
template <typename T> struct TypeWrapper {};
using Return = typename std::result_of<Fun()>::type;
public:
FinallyHelper(Fun body) {
try {
execute(TypeWrapper<Return>(), body);
}
catch(...) {
m_promise.set_exception(std::current_exception());
}
}
Return get() {
return m_promise.get_future().get();
}
private:
template <typename T>
void execute(T, Fun body) {
m_promise.set_value(body());
}
void execute(TypeWrapper<void>, Fun body) {
body();
}
std::promise<Return> m_promise;
};
template <typename Fun>
FinallyHelper<Fun> make_finally_helper(Fun body) {
return FinallyHelper<Fun>(body);
}
Luego está la macro real.
#define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try
#define finally }); /
true; /
({return __finally_helper.get();})) /
/***/
Se puede usar así:
void test() {
try_with_finally {
raise_exception();
}
catch(const my_exception1&) {
/*...*/
}
catch(const my_exception2&) {
/*...*/
}
finally {
clean_it_all_up();
}
}
El uso de std::promise
hace que sea muy fácil de implementar, pero probablemente también introduce un poco de sobrecarga innecesaria que se podría evitar al reimplementar solo las funcionalidades necesarias de std::promise
.
¹ CAVEAT: hay algunas cosas que no funcionan como la versión Java de finally
. La parte superior de mi cabeza:
- no es posible separarse de un bucle externo con la instrucción
break
dentro de los bloquestry
ycatch()
, ya que viven dentro de una función lambda; - debe haber al menos un bloque
catch()
después deltry
: es un requisito de C ++; - Si la función tiene un valor de retorno distinto a void pero no hay retorno dentro de los bloques
try
ycatch()''s
, la compilación fallará porque la macrofinally
se expandirá al código que querrá devolver unvoid
. Esto podría ser, errado, anulado por tener una macrofinally_noreturn
de tipo.
En general, no sé si alguna vez usaría esto, pero fue divertido jugar con él. :)
Tengo un caso de uso en el que creo que finally
debería ser una parte perfectamente aceptable del lenguaje C ++ 11, ya que creo que es más fácil de leer desde el punto de vista del flujo. Mi caso de uso es una cadena de subprocesos productor / productor, donde se envía un nullptr
centinela al final de la ejecución para cerrar todos los subprocesos.
Si C ++ lo admitiera, querría que su código se vea así:
extern Queue downstream, upstream;
int Example()
{
try
{
while(!ExitRequested())
{
X* x = upstream.pop();
if (!x) break;
x->doSomething();
downstream.push(x);
}
}
finally {
downstream.push(nullptr);
}
}
Creo que esto es más lógico que poner tu declaración de fin al comienzo del bucle, ya que ocurre después de que el bucle haya salido ... pero eso es una ilusión porque no podemos hacerlo en C ++. Tenga en cuenta que la cola downstream
está conectada a otro hilo, por lo que no puede poner el push(nullptr)
centinela push(nullptr)
en el destructor de downstream
porque no se puede destruir en este punto ... debe permanecer vivo hasta que el otro hilo recibe el nullptr
.
Así que aquí es cómo usar una clase RAII con lambda para hacer lo mismo:
class Finally
{
public:
Finally(std::function<void(void)> callback) : callback_(callback)
{
}
~Finally()
{
callback_();
}
std::function<void(void)> callback_;
};
Y aquí es cómo lo usas:
extern Queue downstream, upstream;
int Example()
{
Finally atEnd([](){
downstream.push(nullptr);
});
while(!ExitRequested())
{
X* x = upstream.pop();
if (!x) break;
x->doSomething();
downstream.push(x);
}
}
try
{
...
goto finally;
}
catch(...)
{
...
goto finally;
}
finally:
{
...
}