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 supprimerconst
decommit()
- 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
etb
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
etb
sont temporaires ont à voir avecinline 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).
Vous devez vous connecter pour publier un commentaire.
Coup de pouce.ScopeExit est une macro qui doit travailler avec les non-C++11 code, c'est à dire un code qui n'a pas accès à des lambdas dans la langue. Il utilise astucieux modèle de hacks (comme d'abuser de l'ambiguïté résultant de l'utilisation
<
pour les deux modèles et les opérateurs de comparaison!) et le préprocesseur à émuler les lambda fonctions. C'est pourquoi le code est plus long.Le code indiqué est aussi buggé (qui est sans doute la plus forte raison de l'utilisation d'une solution existante): il invoque un comportement indéterminé en raison de retourner des références à des temporaires.
Puisque vous essayez d'utiliser le C++11 caractéristiques, le code peut être amélioré par l'utilisation de la sémantique de déplacement, les références rvalue et parfait de transfert:
std::forward<AdquireLambda>(_al)();
, quelle est la différence entre cela et tout simplement faire_al();
à l'intérieur du constructeur de déplacement?struct foo { void operator()() && /* not the rvalue qualifier */; };
.Visual Studio 2015 Update 3
a un très faible taux de bug lié à la capture d'arguments. Parfois, ce genre d'arguments trouvé non initialisée au moment de l'utilisation dans un lambda et donc conduit à la violation d'accès. Parce queBoost.ScopeExit
utilise la même approche (arguments de la capture par l' []-bloc), puis il a le même bug. Solution de contournement consiste à remplacer la capture explicitement par le paramètre lambda.Lambda
peut se lier à une lvalue de référence, dans le mouvement ctor,std::move(that.rollbackLambda)
doit utiliserstd::forward
à la place! Cela montre l'importance du dépistage. Pour un testées mise en œuvre voir cette réponseEncore plus court: je ne sais pas pourquoi les gars, vous insistez sur la mise le modèle de la garde de la classe.
Note qu'il est essentiel que le code de nettoyage ne les jetez pas, sinon vous êtes dans des situations similaires, comme à jeter des destructeurs.
Utilisation:
Ma source d'inspiration a été la même DrDobbs article comme pour l'OP.
Modifier 2017/2018: Après observation de (certains de) Andrei présentation qu'André liés (j'ai sauté à la fin où il est dit: "Douloureusement Proche de l'Idéal!") J'ai réalisé que c'est faisable. La plupart du temps vous ne voulez pas avoir des protections supplémentaires pour tout. Vous venez de faire des choses, et à la fin il réussit ou de restauration devrait se produire.
Modifier 2018: Ajout de l'exécution de la politique qui a supprimé la nécessité de la
dismiss
appel.Utilisation:
std::function
, il va engager l'allocation dynamique.scope_guard guard1 = revert1;
.throw()
est déconseillée en C++11,noexcept
le remplace.function::operator=(nullptr_t)
n'est pas noexcept pré-C++17. Ainsi, pré-C++17, votre constructeur de déplacement pouvez jeter àother.f = nullptr
, ce qui serait susceptible d'empêcher la fonction nettoyage de l'exécution (cela dépend de l'état de l'déplacé à d'autres.f). Aussi, rejeter() peut résilier() carf = nullptr
pouvez (essayer) de le jeter. Peut-être que ce ne sont pas des problèmes dans la pratique, mais vous pourriez avoir autour d'eux en utilisant un autre drapeau à la place d'une valeur null.funtion::operator=(nullptr_t)
est noexcept depuis C++11, voir 20.8.11.2.1[func.wrap.func.con]
. Vous êtes à droite avec le constructeur de déplacement et emplace, mais en réalité, il ne va pas à l'échec. Si vous voulez être sûr que vous pouvez ajouter le nettoyage avant l'opération et vérifier dans le nettoyage si l'opération a été effectivement fait.Callable
destructeur de jette (dans ce cas, il n'y a rien à faire) je ne vois pas comment lenullptr_t
affectation peut jeter. Je pense que c'est pourquoi ils l'ont ajouté dans C++17, parce que toutes les implémentations ont été de facto noexcept.operator +=
est juste une méthode de convenance. Je pense qu'il n'y a rien de mal avec l'aide de l'un de l'autredeque
méthodes pour ajouter ou de supprimer ou de parcourir les gestionnaires, donc, je laisse accessible. Vous pourrait-il faire en privé si vous souhaitez limiter l'interface.std::function
. On peut faire, par exemple, un externevector
descope_guard
s, ce qui n'était pas possible avec modélisé définitions.std::function
constructeur peut jeter. Si il jette, le code de nettoyage ne sera pas exécutée.undo_func
pourrait être dans un indéfini déménagé de l'état lorsque le bloc catch est exécuté, mais pratiquement la seule exception rencontrés ici devrait êtrebad_alloc
qui devrait être lancée avant le déménagement.undo_func
, le constructeur de déplacement ne devrait pas jeter, alorsbad_alloc
est possible seulement en cas. La seule chose que je ne l'aime toujours pas, c'est questd::function
peut impliquer des allocations dynamiques qui provoquent des performances. J'ai une solution qui n'utilise passtd::function
(stackoverflow.com/a/48842771/5447906), mais il est plus complexe de mise en œuvre.std::deque
.Vous pourriez être intéressé à voir ce présentation par Andrei lui-même sur sa propre prise sur la manière d'améliorer scopedguard avec c++11
Vous pouvez utiliser
std::unique_ptr
à cette fin, qui met en œuvre le RAII modèle.Par exemple:
La deleter fonction de la
unique_ptr p
rouleaux de l'ancien inséré valeur de retour, si le champ a été laissé pendant une exception est active. Si vous préférez explicite engager, vous pouvez supprimer leuncaugth_exception()
question dans le deleter fonction et ajouter à la fin du blocp.release()
qui libère le pointeur. Voir Démo ici.- Je utiliser cela fonctionne comme un charme, pas de code supplémentaire.
std::bad_alloc
(ou au moins tente de le faire).Il y a une chance cette approche sera normalisée en C++17 ou dans la Bibliothèque Fondamentaux TS par le biais de proposition P0052R0
Sur le premier coup d'œil, cela a le même inconvénient que
std::async
parce que vous avez à stocker la valeur de retour ou le destructeur sera appelé immédiatement et il ne fonctionne pas comme prévu.makeScopeGuard renvoie une référence const. Vous ne pouvez pas stocker cette const référence dans un const ref à l'appelant de côté dans une ligne comme:
De sorte que vous êtes en invoquant un comportement indéterminé.
Herb Sutter GOTW 88 donne un peu de fond sur le stockage des valeurs de constantes références.
return
déclaration. Le temporaire est détruit lorsque la fonction se termine.unused variable 'a' [-Wunused-variable]
utilisation[[gnu::unused]]
attribut:[[gnu::unused]] auto const & a = ...
.Sans engagement de suivi, mais extrêmement soigné et rapide.
Utilisation
FWIW, je pense que Andrei Alexandrescu a utilisé une très soignée de syntaxe dans son CppCon 2015 de parler de "Déclarative de Contrôle de Flux" (vidéo, les diapositives).
Le code suivant est fortement inspiré par elle:
Essayer en Ligne
GitHub Gist
Pour votre cas d'utilisation, vous pourriez également être intéressé par
std::uncaught_exception()
etstd::uncaught_exceptions()
de savoir si votre sortie de la portée "normalement" ou après qu'une exception a été levée:HTH
Vous déjà choisi une réponse, mais je vais relever le défi de toute façon:
Au lieu d'avoir les fonctions de création et un constructeur, vous limité à seulement les fonctions de création, avec le principal constructeur d'être
private
. Je ne pouvais pas comprendre comment limiter lefriend
-ed instanciations à tout ceux impliquant le modèle à paramètre. (Peut-être parce que le paramètre n'est mentionné que dans le type de retour.) Peut-être une solution qui peut être demandé sur ce site. Depuis la première action n'a pas besoin d'être stockée, il est présent uniquement dans les fonctions de création. Il y a un paramètre Booléen pour signaler sithrow
ing à partir de la première action déclenche un roll-back ou pas.La
std::decay
partie des bandes de cv-qualificatifs, et font référence à des marqueurs. Mais vous ne pouvez pas l'utiliser pour cet usage général si le type d'entrée est intégré dans le tableau, car il va appliquer le tableau de pointeur de conversion trop.En voici un autre, maintenant une variation sur @kwarnke de l':
Encore une autre réponse, mais je crains de trouver les autres tous les manque dans un sens ou un autre. Notamment, la accepté de répondre à des dates à partir de 2012, mais il a un bug important (voir ce commentaire). Cela montre l'importance du dépistage.
Ici est une implémentation de l' >=C++11 scope_guard, qui est ouvertement disponibles et de nombreux tests. Il est destiné à être/avoir:
std::function
ou virtuel tableau des pénalités)Voir aussi la liste complète des fonctionnalités.