La plus simple et la plus élégante de c++11 ScopeGuard

Je suis d'essayer d'écrire une simple ScopeGuard basée sur des concepts Alexandrescu mais avec c++11 les expressions idiomatiques.

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

Ici est l'utilisation:

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

Depuis ma version est plus courte que la plupart des exemples (comme Boost ScopeExit) je me demande ce qu'spécialités que je vais la laisser sortir. J'espère que je suis dans un 80/20 scénario ici (où j'ai obtenu 80% de propreté, avec 20 pour cent de lignes de code), mais je ne pouvais pas m'empêcher de me demander si je suis absent quelque chose d'important, ou est-il une lacune vaut la peine de mentionner de cette version de la ScopeGuard idiome

merci!

Modifier j'ai remarqué une question très importante avec la makeScopeGuard qui prend l'acquérir lambda dans le constructeur. Si les acquérir lambda lancers, puis la libération lambda n'est jamais appelée, parce que la portée de la garde n'a jamais été entièrement construite. Dans de nombreux cas, c'est le comportement souhaité, mais j'ai l'impression que, parfois, une version qui invoque la restauration si un jet qui se passe est souhaitée ainsi:

//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;
}

donc, pour être complet, je veux mettre ici le code complet, y compris les tests:


#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();
}
};
}
}
  • Une chose que vous êtes en laissant de côté, c'est que ce code ne compile pas. Les déclarations de la garde des variables manquantes paramètres du modèle.
  • ne pas le compilateur être en mesure d'en déduire les arguments? ce sont des expressions lambda, donc comme vous le savez, leur type est opaque
  • je suis absent la make_scopeGuard, donnez-moi une minute
  • donc je garde le temporaires en const références, et changé le commis membre mutable donc sa modifiables dans un objet const
  • je ne suis pas votre compilateur. Le code ne compile pas pour les mêmes raisons. Vous pourriez éviter de perdre du temps pour tout le monde si vous avez essayé de compiler votre code avant de le poster.
  • si j'étais vous, je ne voudrais pas faire commited mutables, mais je voudrais supprimer const de commit() - après tout, commit() les changements de l'état de l'objet - et que le changement est important; pourquoi la fonction que changements importante partie de l'état de l'objet serait marqué const?
  • car une référence à une temporaire ne peut être tenu avec const références. Ceci est expliqué dans l'original Alexandrescu article sur ScopeGuard
  • Ou avec une référence rvalue.
  • hein??? Où avez-vous des références dans commit()?
  • je suis désolé, c'est de travailler maintenant
  • les références sont a et b et font référence à deux objets temporaires
  • attendez, peut-être que j'ai oublié quelques chose importante, mais ce n'est le fait que a et b sont temporaires ont à voir avec inline void commit() const { committed = true; }?
  • si commit() ne serait pas un const méthode, je ne pouvais pas l'appeler sur ceux temporaire références. Si le commis ne serait pas mutables, commit() ne peut pas être une méthode const
  • c'est fondamentalement un complexe truc pour éviter la copie par valeur, avant c++11 constructeurs de déplacement, les gens avaient à mettre en œuvre la sémantique de déplacement sur la portée des gardes pour éviter d'avoir temporaires appel libération des rappels à plusieurs reprises. C'était un raccourci pour éviter que la complexité
  • Je viens de poster un version beaucoup plus simple basé sur le même article. Notez que je n'ai pas besoin de générateur de fonctions.
  • Je ne savais pas que cet idiome est appelé l'Étendue de la Garde. J'ai donc créé une question similaire: stackoverflow.com/q/48842770/5447906 (solutions à partir de là peut être utile).

InformationsquelleAutor lurscher | 2012-04-22