c++ lambda c++11 exception-safety scopeguard

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



lambda c++11 (10)

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

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

Aquí está el uso:

void SomeFuncThatShouldBehaveAtomicallyInCaseOfExceptions() { std::vector<int> myVec; std::vector<int> 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 alcance 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< typename aLambda , typename rLambda> ScopeGuard< rLambda > // return by value is the preferred C++11 way. makeScopeGuardThatDoesNOTRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { return ScopeGuard< rLambda >( std::forward<aLambda>(_a) , std::forward<rLambda>(_r )); // *** no longer UB, because we''re returning by value } template< typename aLambda , typename rLambda> ScopeGuard< rLambda > // return by value is the preferred C++11 way. makeScopeGuardThatDoesRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { auto scope = ScopeGuard< rLambda >(std::forward<rLambda>(_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 <vector> namespace RAII { template< typename Lambda > 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< typename AdquireLambda > ScopeGuard( const AdquireLambda& _al , const Lambda& _l) : committed(false) , rollbackLambda(_l) { std::forward<AdquireLambda>(_al)(); } //WARNING: only safe if adquire lambda does not throw, otherwise release lambda is never invoked, because the scope guard never finished initialistion.. template< typename AdquireLambda, typename L > ScopeGuard( AdquireLambda&& _al , L&& _l) : committed(false) , rollbackLambda(std::forward<L>(_l)) { std::forward<AdquireLambda>(_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< typename aLambda , typename rLambda> ScopeGuard< rLambda > // return by value is the preferred C++11 way. makeScopeGuardThatDoesNOTRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { return ScopeGuard< rLambda >( std::forward<aLambda>(_a) , std::forward<rLambda>(_r )); // *** no longer UB, because we''re returning by value } template< typename aLambda , typename rLambda> ScopeGuard< rLambda > // return by value is the preferred C++11 way. makeScopeGuardThatDoesRollbackIfAdquireThrows( aLambda&& _a , rLambda&& _r) // again perfect forwarding { auto scope = ScopeGuard< rLambda >(std::forward<rLambda>(_r )); // *** no longer UB, because we''re returning by value _a(); return scope; } template<typename rLambda> ScopeGuard< rLambda > makeScopeGuard(rLambda&& _r) { return ScopeGuard< rLambda >( std::forward<rLambda>(_r )); } namespace basic_usage { struct Test { std::vector<int> myVec; std::vector<int> 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(); } }; } }


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

#include <functional> class scope_guard { public: template<class Callable> scope_guard(Callable && undo_func) try : f(std::forward<Callable>(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<void()> 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 <functional> #include <deque> class scope_guard { public: enum execution { always, no_exception, exception }; scope_guard(scope_guard &&) = default; explicit scope_guard(execution policy = always) : policy(policy) {} template<class Callable> scope_guard(Callable && func, execution policy = always) : policy(policy) { this->operator += <Callable>(std::forward<Callable>(func)); } template<class Callable> scope_guard& operator += (Callable && func) try { handlers.emplace_front(std::forward<Callable>(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<std::function<void()>> 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(); }; // ...


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 ) );


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 <typename L, DisableIf<std::is_same<RemoveReference<RemoveCv<L>>, ScopeGuard<Lambda>>> =_ > /* see http://loungecpp.net/w/EnableIf_in_C%2B%2B11 * and http://.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>(_l)) // avoid copying unless necessary {} template< typename AdquireLambda, typename L > ScopeGuard( AdquireLambda&& _al , L&& _l) : committed(false) , rollbackLambda(std::forward<L>(_l)) { std::forward<AdquireLambda>(_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<aLambda>(_a) , std::forward<rLambda>(_r )); // *** no longer UB, because we''re returning by value } template<typename rLambda> ScopeGuard< rLambda > makeScopeGuard(rLambda&& _r) { return ScopeGuard< rLambda >( std::forward<rLambda>(_r )); }


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 <typename EF> scope_exit<see below> make_scope_exit(EF &&exit_function) noexcept; template <typename EF> scope_exit<see below> make_scope_fail(EF && exit_function) noexcept; template <typename EF> scope_exit<see below> 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.


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<int> v{}; v.push_back(42); unique_ptr<decltype(v), function<void(decltype(v)*)>> 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 alcance 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 Demo aquí.


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

template <typename F> struct ScopeExit { ScopeExit(F&& f) : m_f(std::forward<F>(f)) {} ~ScopeExit() { m_f(); } F m_f; }; template <typename F> ScopeExit<F> makeScopeExit(F&& f) { return ScopeExit<F>(std::forward<F>(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


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

#include <iostream> #include <type_traits> #include <utility> template < typename RollbackLambda > class ScopeGuard; template < typename RollbackLambda > auto make_ScopeGuard( RollbackLambda &&r ) -> ScopeGuard<typename std::decay<RollbackLambda>::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<typename std::decay<AnyRollbackLambda>::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<typename std::decay<RollbackLambda>::type> { using std::forward; return ScopeGuard<typename std::decay<RollbackLambda>::type>{ forward<RollbackLambda>(r) }; } template < typename ActionLambda, typename RollbackLambda > auto make_ScopeGuard( ActionLambda && a, RollbackLambda &&r, bool roll_back_if_action_throws ) -> ScopeGuard<typename std::decay<RollbackLambda>::type> { using std::forward; if ( not roll_back_if_action_throws ) forward<ActionLambda>(a)(); auto result = make_ScopeGuard( forward<RollbackLambda>(r) ); if ( roll_back_if_action_throws ) forward<ActionLambda>(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.


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

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


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.