El más simple y más limpio c ++ 11 ScopeGuard

Intento escribir un ScopeGuard simple basado en los conceptos de Alexandrescu pero con modismos de C ++ 11.

namespace RAII { template class ScopeGuard { mutable bool committed; Lambda rollbackLambda; public: ScopeGuard( const Lambda& _l) : committed(false) , rollbackLambda(_l) {} template ScopeGuard( const AdquireLambda& _al , const Lambda& _l) : committed(false) , rollbackLambda(_l) { _al(); } ~ScopeGuard() { if (!committed) rollbackLambda(); } inline void commit() const { committed = true; } }; template const ScopeGuard& makeScopeGuard( const aLambda& _a , const rLambda& _r) { return ScopeGuard( _a , _r ); } template const ScopeGuard& makeScopeGuard(const rLambda& _r) { return ScopeGuard(_r ); } } 

Aquí está el uso:

 void SomeFuncThatShouldBehaveAtomicallyInCaseOfExceptions() { std::vector myVec; std::vector someOtherVec; myVec.push_back(5); //first constructor, adquire happens elsewhere const auto& a = RAII::makeScopeGuard( [&]() { myVec.pop_back(); } ); //sintactically neater, since everything happens in a single line const auto& b = RAII::makeScopeGuard( [&]() { someOtherVec.push_back(42); } , [&]() { someOtherVec.pop_back(); } ); b.commit(); a.commit(); } 

Como mi versión es mucho más corta que la mayoría de los ejemplos (como Boost ScopeExit), me pregunto qué especialidades dejo fuera. Espero estar en un escenario 80/20 aquí (donde obtuve un 80 por ciento de pulcritud con el 20 por ciento de las líneas de código), pero no pude evitar preguntarme si me estoy perdiendo algo importante, o si hay alguna deficiencia que valga la pena. mencionando esta versión del modismo de ScopeGuard

¡Gracias!

Editar Noté un problema muy importante con el makeScopeGuard que toma el lambda de adquisición en el constructor. Si adquiere lambda lanza, entonces el lanzamiento lambda nunca se llama, porque el protector de scope nunca se construyó completamente. En muchos casos, este es el comportamiento deseado, pero creo que a veces también se desea una versión que invocará la reversión si ocurre un lanzamiento.

 //WARNING: only safe if adquire lambda does not throw, otherwise release lambda is never invoked, because the scope guard never finished initialistion.. template ScopeGuard // return by value is the preferred C++11 way. makeScopeGuardThatDoesNOTRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { return ScopeGuard( std::forward(_a) , std::forward(_r )); // *** no longer UB, because we're returning by value } template ScopeGuard // return by value is the preferred C++11 way. makeScopeGuardThatDoesRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { auto scope = ScopeGuard(std::forward(_r )); // *** no longer UB, because we're returning by value _a(); return scope; } 

entonces para completar, quiero poner aquí el código completo, que incluye pruebas:


 #include  namespace RAII { template class ScopeGuard { bool committed; Lambda rollbackLambda; public: ScopeGuard( const Lambda& _l) : committed(false) , rollbackLambda(_l) {} ScopeGuard( const ScopeGuard& _sc) : committed(false) , rollbackLambda(_sc.rollbackLambda) { if (_sc.committed) committed = true; else _sc.commit(); } ScopeGuard( ScopeGuard&& _sc) : committed(false) , rollbackLambda(_sc.rollbackLambda) { if (_sc.committed) committed = true; else _sc.commit(); } //WARNING: only safe if adquire lambda does not throw, otherwise release lambda is never invoked, because the scope guard never finished initialistion.. template ScopeGuard( const AdquireLambda& _al , const Lambda& _l) : committed(false) , rollbackLambda(_l) { std::forward(_al)(); } //WARNING: only safe if adquire lambda does not throw, otherwise release lambda is never invoked, because the scope guard never finished initialistion.. template ScopeGuard( AdquireLambda&& _al , L&& _l) : committed(false) , rollbackLambda(std::forward(_l)) { std::forward(_al)(); // just in case the functor has &&-qualified operator() } ~ScopeGuard() { if (!committed) rollbackLambda(); } inline void commit() { committed = true; } }; //WARNING: only safe if adquire lambda does not throw, otherwise release lambda is never invoked, because the scope guard never finished initialistion.. template ScopeGuard // return by value is the preferred C++11 way. makeScopeGuardThatDoesNOTRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { return ScopeGuard( std::forward(_a) , std::forward(_r )); // *** no longer UB, because we're returning by value } template ScopeGuard // return by value is the preferred C++11 way. makeScopeGuardThatDoesRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { auto scope = ScopeGuard(std::forward(_r )); // *** no longer UB, because we're returning by value _a(); return scope; } template ScopeGuard makeScopeGuard(rLambda&& _r) { return ScopeGuard( std::forward(_r )); } namespace basic_usage { struct Test { std::vector myVec; std::vector someOtherVec; bool shouldThrow; void run() { shouldThrow = true; try { SomeFuncThatShouldBehaveAtomicallyInCaseOfExceptionsUsingScopeGuardsThatDoesNOTRollbackIfAdquireThrows(); } catch (...) { AssertMsg( myVec.size() == 0 && someOtherVec.size() == 0 , "rollback did not work"); } shouldThrow = false; SomeFuncThatShouldBehaveAtomicallyInCaseOfExceptionsUsingScopeGuardsThatDoesNOTRollbackIfAdquireThrows(); AssertMsg( myVec.size() == 1 && someOtherVec.size() == 1 , "unexpected end state"); shouldThrow = true; myVec.clear(); someOtherVec.clear(); try { SomeFuncThatShouldBehaveAtomicallyInCaseOfExceptionsUsingScopeGuardsThatDoesRollbackIfAdquireThrows(); } catch (...) { AssertMsg( myVec.size() == 0 && someOtherVec.size() == 0 , "rollback did not work"); } } void SomeFuncThatShouldBehaveAtomicallyInCaseOfExceptionsUsingScopeGuardsThatDoesNOTRollbackIfAdquireThrows() //throw() { myVec.push_back(42); auto a = RAII::makeScopeGuard( [&]() { HAssertMsg( myVec.size() > 0 , "attempt to call pop_back() in empty myVec"); myVec.pop_back(); } ); auto b = RAII::makeScopeGuardThatDoesNOTRollbackIfAdquireThrows( [&]() { someOtherVec.push_back(42); } , [&]() { HAssertMsg( myVec.size() > 0 , "attempt to call pop_back() in empty someOtherVec"); someOtherVec.pop_back(); } ); if (shouldThrow) throw 1; b.commit(); a.commit(); } void SomeFuncThatShouldBehaveAtomicallyInCaseOfExceptionsUsingScopeGuardsThatDoesRollbackIfAdquireThrows() //throw() { myVec.push_back(42); auto a = RAII::makeScopeGuard( [&]() { HAssertMsg( myVec.size() > 0 , "attempt to call pop_back() in empty myVec"); myVec.pop_back(); } ); auto b = RAII::makeScopeGuardThatDoesRollbackIfAdquireThrows( [&]() { someOtherVec.push_back(42); if (shouldThrow) throw 1; } , [&]() { HAssertMsg( myVec.size() > 0 , "attempt to call pop_back() in empty someOtherVec"); someOtherVec.pop_back(); } ); b.commit(); a.commit(); } }; } } 

Boost.ScopeExit es una macro que necesita trabajar con código que no sea C ++ 11, es decir, código que no tiene acceso a lambdas en el idioma. Utiliza algunos hacks de plantillas inteligentes (como abusar de la ambigüedad que surge del uso de < tanto para las plantillas como para los operadores de comparación) y el preprocesador para emular las características de lambda. Es por eso que el código es más largo.

El código que se muestra también tiene errores (que probablemente sea la razón más fuerte para usar una solución existente): invoca un comportamiento indefinido debido a las referencias de retorno a los temporales.

Como está intentando usar las características de C ++ 11, el código podría mejorarse mucho mediante el uso de semántica de movimiento, referencias de valor r y reenvío perfecto:

 template< typename Lambda > class ScopeGuard { bool committed; // not mutable Lambda rollbackLambda; public: // make sure this is not a copy ctor template >, ScopeGuard>> =_ > /* see http://loungecpp.net/w/EnableIf_in_C%2B%2B11 * and http://stackoverflow.com/q/10180552/46642 for info on DisableIf */ explicit ScopeGuard(L&& _l) // explicit, unless you want implicit conversions from *everything* : committed(false) , rollbackLambda(std::forward(_l)) // avoid copying unless necessary {} template< typename AdquireLambda, typename L > ScopeGuard( AdquireLambda&& _al , L&& _l) : committed(false) , rollbackLambda(std::forward(_l)) { std::forward(_al)(); // just in case the functor has &&-qualified operator() } // move constructor ScopeGuard(ScopeGuard&& that) : committed(that.committed) , rollbackLambda(std::move(that.rollbackLambda)) { that.committed = true; } ~ScopeGuard() { if (!committed) rollbackLambda(); // what if this throws? } void commit() { committed = true; } // no need for const }; template< typename aLambda , typename rLambda> ScopeGuard< rLambda > // return by value is the preferred C++11 way. makeScopeGuard( aLambda&& _a , rLambda&& _r) // again perfect forwarding { return ScopeGuard< rLambda >( std::forward(_a) , std::forward(_r )); // *** no longer UB, because we're returning by value } template ScopeGuard< rLambda > makeScopeGuard(rLambda&& _r) { return ScopeGuard< rLambda >( std::forward(_r )); } 

Aún más corto: no sé por qué ustedes insisten en poner la plantilla en la clase de guardia.

 #include  class scope_guard { public: template scope_guard(Callable && undo_func) try : f(std::forward(undo_func)) { } catch(...) { undo_func(); throw; } scope_guard(scope_guard && other) : f(std::move(other.f)) { other.f = nullptr; } ~scope_guard() { if(f) f(); // must not throw } void dismiss() noexcept { f = nullptr; } scope_guard(const scope_guard&) = delete; void operator = (const scope_guard&) = delete; private: std::function f; }; 

Tenga en cuenta que es esencial que el código de limpieza no arroje, de lo contrario se encontrará en situaciones similares a las de los destructores.

Uso:

 // do step 1 step1(); scope_guard guard1 = [&]() { // revert step 1 revert1(); }; // step 2 step2(); guard1.dismiss(); 

Mi inspiración fue el mismo artículo de DrDobbs que para el OP.


Editar 2017/2018: Después de ver (algunas de) la presentación de Andrei a la que André se relacionó (salté hasta el final donde decía “¡Dolorosamente cerca del ideal!”) Me di cuenta de que era factible. La mayoría de las veces no quieres tener guardias adicionales para todo. Simplemente haces cosas, y al final o tiene éxito o la reversión debería suceder.

Editar 2018: Política de ejecución añadida que eliminó la necesidad de la llamada de dismiss .

 #include  #include  class scope_guard { public: enum execution { always, no_exception, exception }; scope_guard(scope_guard &&) = default; explicit scope_guard(execution policy = always) : policy(policy) {} template scope_guard(Callable && func, execution policy = always) : policy(policy) { this->operator += (std::forward(func)); } template scope_guard& operator += (Callable && func) try { handlers.emplace_front(std::forward(func)); return *this; } catch(...) { if(policy != no_exception) func(); throw; } ~scope_guard() { if(policy == always || (std::uncaught_exception() == (policy == exception))) { for(auto &f : handlers) try { f(); // must not throw } catch(...) { /* std::terminate(); ? */ } } } void dismiss() noexcept { handlers.clear(); } private: scope_guard(const scope_guard&) = delete; void operator = (const scope_guard&) = delete; std::deque> handlers; execution policy = always; }; 

Uso:

 scope_guard scope_exit, scope_fail(scope_guard::execution::exception); action1(); scope_exit += [](){ cleanup1(); }; scope_fail += [](){ rollback1(); }; action2(); scope_exit += [](){ cleanup2(); }; scope_fail += [](){ rollback2(); }; // ... 

Puede que te interese ver esta presentación del propio Andrei por su cuenta sobre cómo mejorar scopedguard con c ++ 11

Puede usar std::unique_ptr para ese fin que implementa el patrón RAII. Por ejemplo:

 vector v{}; v.push_back(42); unique_ptr> p{&v, [] (decltype(v)* v) { if (uncaught_exception()) { v->pop_back(); }}}; throw exception(); // rollback p.release(); // explicit commit 

La función de unique_ptr p tira el valor insertado anteriormente, si el scope se dejó mientras una excepción está activa. Si prefiere una confirmación explícita, puede eliminar la pregunta uncaugth_exception() en la función de eliminación y agregar al final del bloque p.release() que libera el puntero. Vea la demostración aquí.

Existe la posibilidad de que este enfoque se estandarice en C ++ 17 o en los TS de Fundamentos de la Biblioteca a través de la propuesta P0052R0

 template  scope_exit make_scope_exit(EF &&exit_function) noexcept; template  scope_exit make_scope_fail(EF && exit_function) noexcept; template  scope_exit make_scope_success(EF && exit_function) noexcept; 

A primera vista, tiene la misma advertencia que std::async porque tiene que almacenar el valor de retorno o se llamará inmediatamente al destructor y no funcionará como se esperaba.

makeScopeGuard devuelve una referencia constante. No puede almacenar esta referencia constante en una referencia constante en el lado de la persona que llama en una línea como:

 const auto& a = RAII::makeScopeGuard( [&]() { myVec.pop_back(); } ); 

Entonces estás invocando un comportamiento indefinido.

Herb Sutter GOTW 88 brinda algunos antecedentes sobre el almacenamiento de valores en referencias de referencias.

Sin seguimiento de compromiso, pero extremadamente ordenado y rápido.

 template  struct ScopeExit { ScopeExit(F&& f) : m_f(std::forward(f)) {} ~ScopeExit() { m_f(); } F m_f; }; template  ScopeExit makeScopeExit(F&& f) { return ScopeExit(std::forward(f)); }; #define STRING_JOIN(arg1, arg2) STRING_JOIN2(arg1, arg2) #define STRING_JOIN2(arg1, arg2) arg1 ## arg2 #define ON_SCOPE_EXIT(code) auto STRING_JOIN(scopeExit, __LINE__) = makeScopeExit([&](){code;}) 

Uso

 { puts("a"); auto _ = makeScopeExit([]() { puts("b"); }); // More readable with a macro ON_SCOPE_EXIT(puts("c")); } # prints a, c, b 

Yo uso esto funciona como un encanto, sin código adicional.

 shared_ptr x(NULL, [&](int *) { CloseResource(); }); 

Ya elegiste una respuesta, pero de todos modos tomaré el desafío:

 #include  #include  #include  template < typename RollbackLambda > class ScopeGuard; template < typename RollbackLambda > auto make_ScopeGuard( RollbackLambda &&r ) -> ScopeGuard::type>; template < typename RollbackLambda > class ScopeGuard { // The input may have any of: cv-qualifiers, l-value reference, or both; // so I don't do an exact template match. I want the return to be just // "ScopeGuard," but I can't figure it out right now, so I'll make every // version a friend. template < typename AnyRollbackLambda > friend auto make_ScopeGuard( AnyRollbackLambda && ) -> ScopeGuard::type>; public: using lambda_type = RollbackLambda; private: // Keep the lambda, of course, and if you really need it at the end bool committed; lambda_type rollback; // Keep the main constructor private so regular creation goes through the // external function. explicit ScopeGuard( lambda_type rollback_action ) : committed{ false }, rollback{ std::move(rollback_action) } {} public: // Do allow moves ScopeGuard( ScopeGuard &&that ) : committed{ that.committed }, rollback{ std::move(that.rollback) } { that.committed = true; } ScopeGuard( ScopeGuard const & ) = delete; // Cancel the roll-back from being called. void commit() { committed = true; } // The magic happens in the destructor. // (Too bad that there's still no way, AFAIK, to reliably check if you're // already in exception-caused stack unwinding. For now, we just hope the // roll-back doesn't throw.) ~ScopeGuard() { if (not committed) rollback(); } }; template < typename RollbackLambda > auto make_ScopeGuard( RollbackLambda &&r ) -> ScopeGuard::type> { using std::forward; return ScopeGuard::type>{ forward(r) }; } template < typename ActionLambda, typename RollbackLambda > auto make_ScopeGuard( ActionLambda && a, RollbackLambda &&r, bool roll_back_if_action_throws ) -> ScopeGuard::type> { using std::forward; if ( not roll_back_if_action_throws ) forward(a)(); auto result = make_ScopeGuard( forward(r) ); if ( roll_back_if_action_throws ) forward(a)(); return result; } int main() { auto aa = make_ScopeGuard( []{std::cout < < "Woah" << '\n';} ); int b = 1; try { auto bb = make_ScopeGuard( [&]{b *= 2; throw b;}, [&]{b = 0;}, true ); } catch (...) {} std::cout << b++ << '\n'; try { auto bb = make_ScopeGuard( [&]{b *= 2; throw b;}, [&]{b = 0;}, false ); } catch (...) {} std::cout << b++ << '\n'; return 0; } // Should write: "0", "2", and "Woah" in that order on separate lines. 

En lugar de tener funciones de creación y un constructor, usted limita a solo las funciones de creación, siendo el constructor principal private . No pude encontrar la forma de limitar las instancias de friend -ed a las que implican el parámetro de plantilla actual. (Tal vez porque el parámetro se menciona solo en el tipo de devolución.) Tal vez se pueda solicitar una solución en este sitio. Como la primera acción no necesita almacenarse, solo está presente en las funciones de creación. Hay un parámetro booleano para señalar si throw desde la primera acción desencadena un retroceso o no.

La parte std::decay elimina los calificadores cv y los marcadores de referencia. Pero no puede usarlo para este propósito general si el tipo de entrada es una matriz incorporada, ya que también aplicará la conversión de matriz a puntero.

Aquí hay otro, ahora una variación de @ kwarnke:

 std::vector< int > v{ }; v.push_back( 42 ); auto guard_handler = [ & v ] ( nullptr_t ptr ) { v.pop_back( ); }; std::shared_ptr< decltype( guard_handler ) > guard( nullptr , std::move( guard_handler ) ); 

Sin embargo, otra respuesta, pero me temo que me parece que todos los demás carecen de una manera u otra. En particular, la respuesta aceptada data de 2012, pero tiene un error importante (ver este comentario ). Esto muestra la importancia de las pruebas.

Aquí hay una implementación de un> = C ++ 11 scope_guard que está abiertamente disponible y ampliamente probado. Está destinado a ser / tener:

  • moderno, elegante, simple (principalmente interfaz de una sola función y sin macros)
  • general (acepta cualquier llamamiento que respete las condiciones previas)
  • cuidadosamente documentado
  • envoltura de callback delgada (sin std::function agregada o sanciones de tabla virtual)
  • especificaciones de excepción apropiadas

Ver también la lista completa de características .