Najprostszy i najładniejszy c ++ 11 ScopeGuard
Próbuję napisać prostyScopeGuard oparty na koncepcjach Alexandrescu ale z idiomami c ++ 11.
<code>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 ); } } </code>
Oto użycie:
<code>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(); } </code>
Ponieważ moja wersja jest znacznie krótsza niż większość dostępnych tam przykładów (np. Boost ScopeExit), zastanawiam się, jakie specjalności pomijam. Mam nadzieję, że jestem tutaj w scenariuszu 80/20 (gdzie mam 80 procent schludności z 20 procentami linii kodu), ale nie mogłem się powstrzymać od zastanowienia, czy brakuje mi czegoś ważnego, czy też jest jakaś niedobra wartość wspominając o tej wersji idiomu ScopeGuard
dzięki!
Edytować Zauważyłem bardzo ważną kwestię w makeScopeGuard, która pobiera wymaganą lambdę w konstruktorze. Jeśli żądana lambda wyrzuca, to nigdy nie jest wywoływana wersja lambda, ponieważ strażnik zakresu nigdy nie został w pełni skonstruowany. W wielu przypadkach jest to pożądane zachowanie, ale czuję, że czasami pożądana jest wersja, która wywoła wycofanie, jeśli nastąpi rzut:
<code>//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; } </code>
więc, dla kompletności, chcę tutaj umieścić pełny kod, w tym testy:
<code>#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(); } }; } } </code>