C ++ prend-il en charge les blocs «enfin»? (Et de quoi parle ce 'RAII'?)


Réponses:


273

Non, C ++ ne prend pas en charge les blocs «enfin». La raison en est que C ++ prend en charge RAII: "L'acquisition de ressources est l'initialisation" - un mauvais nom pour un concept vraiment utile.

L'idée est que le destructeur d'un objet est responsable de la libération des ressources. Lorsque l'objet a une durée de stockage automatique, le destructeur de l'objet sera appelé lorsque le bloc dans lequel il a été créé se termine - même lorsque ce bloc est quitté en présence d'une exception. Voici l'explication de Bjarne Stroustrup sur le sujet.

Une utilisation courante de RAII est le verrouillage d'un mutex:

// A class with implements RAII
class lock
{
    mutex &m_;

public:
    lock(mutex &m)
      : m_(m)
    {
        m.acquire();
    }
    ~lock()
    {
        m_.release();
    }
};

// A class which uses 'mutex' and 'lock' objects
class foo
{
    mutex mutex_; // mutex for locking 'foo' object
public:
    void bar()
    {
        lock scopeLock(mutex_); // lock object.

        foobar(); // an operation which may throw an exception

        // scopeLock will be destructed even if an exception
        // occurs, which will release the mutex and allow
        // other functions to lock the object and run.
    }
};

RAII simplifie également l'utilisation d'objets en tant que membres d'autres classes. Lorsque la classe propriétaire 'est détruite, la ressource gérée par la classe RAII est libérée car le destructeur de la classe gérée RAII est appelé en conséquence. Cela signifie que lorsque vous utilisez RAII pour tous les membres d'une classe qui gèrent des ressources, vous pouvez utiliser un destructeur très simple, peut-être même par défaut, pour la classe propriétaire, car il n'a pas besoin de gérer manuellement les durées de vie de ses ressources membres. . (Merci à Mike B de l' avoir signalé.)

Pour ceux qui connaissent C # ou VB.NET, vous pouvez reconnaître que RAII est similaire à la destruction déterministe .NET en utilisant des instructions IDisposable et «using» . En effet, les deux méthodes sont très similaires. La principale différence est que RAII libérera de manière déterministe tout type de ressource, y compris la mémoire. Lors de l'implémentation d'IDisposable dans .NET (même le langage .NET C ++ / CLI), les ressources seront libérées de manière déterministe, à l'exception de la mémoire. Dans .NET, la mémoire n'est pas libérée de façon déterministe; la mémoire n'est libérée que pendant les cycles de récupération de place.

 

† Certaines personnes croient que «la destruction est l'abandon des ressources» est un nom plus précis pour l'idiome RAII.


18
"La destruction est l'abandon des ressources" - DIRR ... Non, ça ne marche pas pour moi. = P
Erik Forbes

14
RAII est bloqué - il n'y a vraiment pas de changement. Essayer de le faire serait stupide. Cependant, vous devez admettre que "l'acquisition de ressources est l'initialisation" est encore un nom assez pauvre.
Kevin

162
SBRM == Scope Bound Resource Management
Johannes Schaub - litb

10
Quiconque possède les compétences nécessaires pour concevoir non seulement des logiciels en général, et encore moins des techniques améliorées, ne peut donner aucune excuse valable pour un acronyme aussi horrible.
Hardryv

54
Cela vous laisse bloqué lorsque vous avez quelque chose à nettoyer qui ne correspond à la durée de vie d'aucun objet C ++. Je suppose que vous vous retrouvez avec Lifetime Equals C ++ Class Liftime Ou sinon ça devient moche (LECCLEOEIGU?).
Warren P

79

En C ++, le finalement n'est PAS requis en raison de RAII.

RAII déplace la responsabilité de la sécurité des exceptions de l'utilisateur de l'objet vers le concepteur (et le réalisateur) de l'objet. Je dirais que c'est le bon endroit car vous n'avez alors besoin d'obtenir une sécurité d'exception correcte qu'une seule fois (dans la conception / mise en œuvre). En utilisant enfin, vous devez obtenir une sécurité d'exception correcte à chaque fois que vous utilisez un objet.

IMO aussi le code semble plus net (voir ci-dessous).

Exemple:

Un objet de base de données. Pour vous assurer que la connexion DB est utilisée, elle doit être ouverte et fermée. En utilisant RAII, cela peut être fait dans le constructeur / destructeur.

C ++ comme RAII

void someFunc()
{
    DB    db("DBDesciptionString");
    // Use the db object.

} // db goes out of scope and destructor closes the connection.
  // This happens even in the presence of exceptions.

L'utilisation de RAII facilite l'utilisation d'un objet DB correctement. L'objet DB se fermera correctement par l'utilisation d'un destructeur, peu importe comment nous essayons de l'abuser.

Java comme enfin

void someFunc()
{
    DB      db = new DB("DBDesciptionString");
    try
    {
        // Use the db object.
    }
    finally
    {
        // Can not rely on finaliser.
        // So we must explicitly close the connection.
        try
        {
            db.close();
        }
        catch(Throwable e)
        {
           /* Ignore */
           // Make sure not to throw exception if one is already propagating.
        }
    }
}

Lors de l'utilisation finale, l'utilisation correcte de l'objet est déléguée à l'utilisateur de l'objet. c'est-à-dire qu'il incombe à l'utilisateur de l'objet de fermer correctement la connexion DB. Vous pouvez maintenant faire valoir que cela peut être fait dans le finaliseur, mais les ressources peuvent avoir une disponibilité limitée ou d'autres contraintes et donc vous voulez généralement contrôler la libération de l'objet et ne pas compter sur le comportement non déterministe du garbage collector.

C'est aussi un exemple simple.
Lorsque vous avez plusieurs ressources à libérer, le code peut devenir compliqué.

Une analyse plus détaillée peut être trouvée ici: http://accu.org/index.php/journals/236


16
// Make sure not to throw exception if one is already propagating.Il est important que les destructeurs C ++ ne lèvent pas d'exceptions également pour cette raison.
Cemafor

10
@Cemafor: La raison pour laquelle C ++ ne lève pas d'exceptions du destructeur est différente de Java. En Java, cela fonctionnera (vous perdez juste l'exception d'origine). En C ++, c'est vraiment mauvais. Mais le point en C ++ est que vous ne devez le faire qu'une seule fois (par le concepteur de la classe) quand il écrit le destructeur. En Java, vous devez le faire au point d'utilisation. Il est donc de la responsabilité de l'utilisateur de la classe d'écrire très souvent la même plaque de chaudière.
Martin York

1
S'il s'agit d'être "nécessaire", vous n'avez pas non plus besoin de RAII. Débarrassons-nous! :-) Blague à part, RAII est très bien pour beaucoup de cas. Ce que RAII fait rend plus lourd, c'est le cas où vous voulez exécuter du code (non lié aux ressources) même si le code ci-dessus est retourné tôt. Pour cela, vous utilisez des gotos ou vous le séparez en deux méthodes.
Trinidad

1
@ Trinidad: Ce n'est pas aussi simple que vous le pensez (car toutes vos suggestions semblent choisir les pires options possibles). C'est pourquoi une question peut être un meilleur endroit pour explorer cela que les commentaires.
Martin York

1
Critiquer le "n'est PAS requis en raison du RAII": il y a de nombreux cas où l'ajout de RAII ad hoc serait trop de code passe-partout à ajouter, et un essai final serait tout simplement extrêmement approprié.
ceztko

63

RAII est généralement meilleur, mais vous pouvez facilement avoir la sémantique enfin en C ++. En utilisant une petite quantité de code.

En outre, les directives de base C ++ donnent enfin.

Voici un lien vers l' implémentation GSL Microsoft et un lien vers l' implémentation Martin Moene

Bjarne Stroustrup a déclaré à plusieurs reprises que tout ce qui se trouve dans le GSL signifiait éventuellement entrer dans la norme. Ce devrait donc être une manière pérenne de l'utiliser enfin .

Vous pouvez facilement vous implémenter si vous le souhaitez, continuez à lire.

En C ++ 11 RAII et lambdas permettent de faire enfin un général:

namespace detail { //adapt to your "private" namespace
template <typename F>
struct FinalAction {
    FinalAction(F f) : clean_{f} {}
   ~FinalAction() { if(enabled_) clean_(); }
    void disable() { enabled_ = false; };
  private:
    F clean_;
    bool enabled_{true}; }; }

template <typename F>
detail::FinalAction<F> finally(F f) {
    return detail::FinalAction<F>(f); }

exemple d'utilisation:

#include <iostream>
int main() {
    int* a = new int;
    auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!\n"; });
    std::cout << "doing something ...\n"; }

la sortie sera:

doing something...
leaving the block, deleting a!

Personnellement, j'ai utilisé cela plusieurs fois pour garantir la fermeture du descripteur de fichier POSIX dans un programme C ++.

Avoir une vraie classe qui gère les ressources et évite ainsi tout type de fuite est généralement mieux, mais cela est finalement utile dans les cas où créer une classe sonne comme une surpuissance.

En outre, je l'aime mieux que les autres langages enfin parce que s'il est utilisé naturellement, vous écrivez le code de fermeture à proximité du code d'ouverture (dans mon exemple, le nouveau et supprimer ) et la destruction suit la construction dans l'ordre LIFO comme d'habitude en C ++. Le seul inconvénient est que vous obtenez une variable automatique que vous n'utilisez pas vraiment et la syntaxe lambda la rend un peu bruyante (dans mon exemple à la quatrième ligne, seul le mot enfin et le bloc {} à droite sont significatifs, le le repos est essentiellement du bruit).

Un autre exemple:

 [...]
 auto precision = std::cout.precision();
 auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } );
 std::cout << std::setprecision(3);

Le membre de désactivation est utile si le finalement doit être appelé uniquement en cas d'échec. Par exemple, vous devez copier un objet dans trois conteneurs différents, vous pouvez configurer enfin pour annuler chaque copie et désactiver une fois toutes les copies réussies. Ce faisant, si la destruction ne peut pas être lancée, vous garantissez une garantie solide.

exemple de désactivation :

//strong guarantee
void copy_to_all(BIGobj const& a) {
    first_.push_back(a);
    auto undo_first_push = finally([first_&] { first_.pop_back(); });

    second_.push_back(a);
    auto undo_second_push = finally([second_&] { second_.pop_back(); });

    third_.push_back(a);
    //no necessary, put just to make easier to add containers in the future
    auto undo_third_push = finally([third_&] { third_.pop_back(); });

    undo_first_push.disable();
    undo_second_push.disable();
    undo_third_push.disable(); }

Si vous ne pouvez pas utiliser C ++ 11, vous pouvez enfin l' avoir , mais le code devient un peu plus long. Définissez simplement une structure avec seulement un constructeur et un destructeur, le constructeur prend des références à tout ce qui est nécessaire et le destructeur fait les actions dont vous avez besoin. C'est essentiellement ce que fait le lambda, fait manuellement.

#include <iostream>
int main() {
    int* a = new int;

    struct Delete_a_t {
        Delete_a_t(int* p) : p_(p) {}
       ~Delete_a_t() { delete p_; std::cout << "leaving the block, deleting a!\n"; }
        int* p_;
    } delete_a(a);

    std::cout << "doing something ...\n"; }

Il pourrait y avoir un problème possible: dans la fonction 'finalement (F f)', il retourne un objet de FinalAction, donc le déconstructeur pourrait être appelé avant de retourner finalement la fonction. Peut-être que nous devrions utiliser std :: function au lieu du modèle F.
user1633272

Notez que FinalActionc'est essentiellement le même que l' ScopeGuardidiome populaire , mais avec un nom différent.
anderas

1
Cette optimisation est-elle sûre?
Nulano

2
@ Paolo.Bolzoni Désolé de ne pas avoir répondu plus tôt, je n'ai pas reçu de notification pour votre commentaire. J'avais peur que le bloc finally (dans lequel j'appelle une fonction DLL) soit appelé avant la fin de la portée (car la variable n'est pas utilisée), mais j'ai depuis trouvé une question sur SO qui a dissipé mes inquiétudes. Je voudrais y faire un lien, mais malheureusement, je ne le trouve plus.
Nulano

1
La fonction disable () est une sorte de verrue sur votre conception autrement propre. Si vous voulez que le finalement soit appelé uniquement en cas d'échec, alors pourquoi ne pas simplement utiliser l'instruction catch? N'est-ce pas pour ça?
user2445507

32

Au-delà de faciliter le nettoyage avec des objets basés sur la pile, RAII est également utile car le même nettoyage «automatique» se produit lorsque l'objet est membre d'une autre classe. Lorsque la classe propriétaire est détruite, la ressource gérée par la classe RAII est nettoyée car le dtor de cette classe est appelé en conséquence.

Cela signifie que lorsque vous atteignez le nirvana RAII et que tous les membres d'une classe utilisent RAII (comme des pointeurs intelligents), vous pouvez vous en tirer avec un dtor très simple (peut-être même par défaut) pour la classe propriétaire car il n'a pas besoin de gérer manuellement son durée de vie des ressources des membres.


C'est un très bon point. +1 pour vous. Cependant, peu d'autres personnes vous ont voté. J'espère que cela ne vous dérange pas que j'ai modifié mon message pour inclure vos commentaires. (Je vous ai bien sûr donné crédit.) Merci! :)
Kevin

30

pourquoi est-ce que même les langues gérées fournissent un bloc final malgré les ressources qui sont désallouées automatiquement par le garbage collector de toute façon?

En fait, les langages basés sur les récupérateurs ont besoin "enfin" de plus. Un garbage collector ne détruit pas vos objets en temps opportun, il ne peut donc pas être utilisé pour nettoyer correctement les problèmes non liés à la mémoire.

En termes de données allouées dynamiquement, beaucoup diraient que vous devriez utiliser des pointeurs intelligents.

Toutefois...

RAII transfère la responsabilité de la sécurité d'exception de l'utilisateur de l'objet au concepteur

Malheureusement, c'est sa propre chute. Les vieilles habitudes de programmation C meurent dur. Lorsque vous utilisez une bibliothèque écrite en C ou dans un style très C, RAII n'aura pas été utilisé. À moins de réécrire l'intégralité du front-end de l'API, c'est exactement ce avec quoi vous devez travailler. Ensuite, le manque de «enfin» mord vraiment.


13
Exactement ... RAII semble agréable d'un point de vue idéal. Mais je dois travailler avec des API C conventionnelles tout le temps (comme les fonctions de style C dans l'API Win32 ...). Il est très courant d'acquérir une ressource qui renvoie une sorte de HANDLE, qui nécessite ensuite une fonction comme CloseHandle (HANDLE) pour nettoyer. Utiliser try ... enfin est un bon moyen de gérer les exceptions possibles. (Heureusement, il semble que shared_ptr avec des suppresseurs personnalisés et C ++ 11 lambdas devrait fournir un soulagement basé sur RAII qui ne nécessite pas d'écrire des classes entières pour encapsuler certaines API que j'utilise uniquement en un seul endroit.).
James Johnston

7
@JamesJohnston, il est très facile d'écrire une classe wrapper qui contient tout type de poignée et fournit la mécanique RAII. ATL en fournit un tas par exemple. Il semble que vous considérez cela comme trop difficile, mais je ne suis pas d'accord, ils sont très petits et faciles à écrire.
Mark Ransom

5
Simple oui, petit non. La taille dépend de la complexité de la bibliothèque avec laquelle vous travaillez.
Philip Couling

1
@MarkRansom: Existe-t-il un mécanisme par lequel RAII peut faire quelque chose d'intelligent si une exception se produit pendant le nettoyage alors qu'une autre exception est en attente? Dans les systèmes avec try / enfin, il est possible - bien que maladroit - d'arranger les choses pour que l'exception en attente et l'exception qui s'est produite pendant le nettoyage soient stockées dans une nouvelle CleanupFailedException. Existe-t-il un moyen plausible d'obtenir un tel résultat en utilisant RAII?
supercat

3
@couling: Il existe de nombreux cas où un programme appellera une SomeObject.DoSomething()méthode et voudra savoir si (1) elle a réussi, (2) a échoué sans effets secondaires , (3) a échoué avec des effets secondaires auxquels l'appelant est prêt à faire face , ou (4) a échoué avec des effets secondaires auxquels l'appelant ne peut pas faire face. Seul l'appelant saura à quelles situations il peut et ne peut pas faire face; ce dont l'appelant a besoin est un moyen de savoir quelle est la situation. Il est dommage qu'il n'y ait pas de mécanisme standard pour fournir les informations les plus importantes sur une exception.
supercat

9

Une autre émulation de bloc "enfin" utilisant les fonctions lambda C ++ 11

template <typename TCode, typename TFinallyCode>
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
    try
    {
        code();
    }
    catch (...)
    {
        try
        {
            finally_code();
        }
        catch (...) // Maybe stupid check that finally_code mustn't throw.
        {
            std::terminate();
        }
        throw;
    }
    finally_code();
}

Espérons que le compilateur optimisera le code ci-dessus.

Maintenant, nous pouvons écrire du code comme ceci:

with_finally(
    [&]()
    {
        try
        {
            // Doing some stuff that may throw an exception
        }
        catch (const exception1 &)
        {
            // Handling first class of exceptions
        }
        catch (const exception2 &)
        {
            // Handling another class of exceptions
        }
        // Some classes of exceptions can be still unhandled
    },
    [&]() // finally
    {
        // This code will be executed in all three cases:
        //   1) exception was not thrown at all
        //   2) exception was handled by one of the "catch" blocks above
        //   3) exception was not handled by any of the "catch" block above
    }
);

Si vous le souhaitez, vous pouvez envelopper cet idiome dans des macros "essayer - enfin":

// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};

#define begin_try    with_finally([&](){ try
#define finally      catch(never_thrown_exception){throw;} },[&]()
#define end_try      ) // sorry for "pascalish" style :(

Maintenant, le bloc "enfin" est disponible en C ++ 11:

begin_try
{
    // A code that may throw
}
catch (const some_exception &)
{
    // Handling some exceptions
}
finally
{
    // A code that is always executed
}
end_try; // Sorry again for this ugly thing

Personnellement, je n'aime pas la version "macro" de l'idiome "finalement" et préférerais utiliser la fonction pure "with_finally" même si une syntaxe est plus volumineuse dans ce cas.

Vous pouvez tester le code ci-dessus ici: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

PS

Si vous avez besoin d'un bloc enfin dans votre code, les gardes de portée ou les macros ON_FINALLY / ON_EXCEPTION répondront probablement mieux à vos besoins.

Voici un court exemple d'utilisation ON_FINALLY / ON_EXCEPTION:

void function(std::vector<const char*> &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    ...

1
La première est pour moi la plus lisible de toutes les options présentées dans cette page. +1
Nikos

7

Désolé d'avoir déterré un tel ancien thread, mais il y a une erreur majeure dans le raisonnement suivant:

RAII déplace la responsabilité de la sécurité des exceptions de l'utilisateur de l'objet vers le concepteur (et le réalisateur) de l'objet. Je dirais que c'est le bon endroit car vous n'avez alors besoin d'obtenir une sécurité d'exception correcte qu'une seule fois (dans la conception / mise en œuvre). En utilisant enfin, vous devez obtenir une sécurité d'exception correcte à chaque fois que vous utilisez un objet.

Plus souvent qu'autrement, vous devez traiter des objets alloués dynamiquement, des nombres dynamiques d'objets, etc. Dans le bloc d'essai, certains codes peuvent créer de nombreux objets (combien sont déterminés au moment de l'exécution) et leur stocker des pointeurs dans une liste. Maintenant, ce n'est pas un scénario exotique, mais très courant. Dans ce cas, vous voudriez écrire des trucs comme

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  finally
  {
    while (!myList.empty())
    {
      delete myList.back();
      myList.pop_back();
    }
  }
}

Bien sûr, la liste elle-même sera détruite lors de la sortie de la portée, mais cela ne nettoiera pas les objets temporaires que vous avez créés.

Au lieu de cela, vous devez emprunter la voie laide:

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  catch(...)
  {
  }

  while (!myList.empty())
  {
    delete myList.back();
    myList.pop_back();
  }
}

Aussi: pourquoi est-ce que même les langages gérés fournissent un bloc final malgré les ressources qui sont de toute façon automatiquement désallouées par le garbage collector?

Astuce: il y a plus que vous pouvez faire avec "finalement" plus qu'une simple désallocation de mémoire.


17
Les langages gérés ont besoin finalement de blocs précisément parce qu'un seul type de ressource est automatiquement géré: la mémoire. RAII signifie que toutes les ressources peuvent être gérées de la même manière, donc pas besoin de finalement. Si vous avez réellement utilisé RAII dans votre exemple (en utilisant des pointeurs intelligents dans votre liste au lieu de nus), le code serait plus simple que votre exemple "enfin". Et encore plus simple si vous ne vérifiez pas la valeur de retour de new - la vérifier est à peu près inutile.
Myto

7
newne renvoie pas NULL, il lève une exception à la place
Hasturkun

5
Vous soulevez une question importante, mais elle a 2 réponses possibles. La première est celle donnée par Myto - utiliser des pointeurs intelligents pour toutes les allocations dynamiques. L'autre consiste à utiliser des conteneurs standard, qui détruisent toujours leur contenu lors de la destruction. Quoi qu'il en soit, chaque objet alloué appartient en dernier ressort à un objet alloué statiquement qui le libère automatiquement lors de la destruction. Il est vraiment dommage que ces meilleures solutions soient difficiles à découvrir pour les programmeurs en raison de la grande visibilité des pointeurs et des tableaux simples.
j_random_hacker

4
C ++ 11 améliore cela et inclut std::shared_ptret std::unique_ptrdirectement dans le stdlib.
u0b34a0f6ae

16
La raison pour laquelle votre exemple est si horrible n'est pas parce que RAII est défectueux, mais plutôt parce que vous ne l'avez pas utilisé. Les pointeurs bruts ne sont pas RAII.
Ben Voigt

6

FWIW, Microsoft Visual C ++ prend en charge try, enfin et il a toujours été utilisé dans les applications MFC comme méthode pour intercepter des exceptions graves qui entraîneraient sinon un plantage. Par exemple;

int CMyApp::Run() 
{
    __try
    {
        int i = CWinApp::Run();
        m_Exitok = MAGIC_EXIT_NO;
        return i;
    }
    __finally
    {
        if (m_Exitok != MAGIC_EXIT_NO)
            FaultHandler();
    }
}

J'ai utilisé cela dans le passé pour faire des choses comme enregistrer des sauvegardes de fichiers ouverts avant de quitter. Certains paramètres de débogage JIT vont cependant briser ce mécanisme.


4
gardez à l'esprit que ce ne sont pas vraiment des exceptions C ++, mais des exceptions SEH. Vous pouvez utiliser les deux dans le code MS C ++. SEH est un gestionnaire d'exception du système d'exploitation qui est la façon dont VB, .NET implémente les exceptions.
gbjbaanb

et vous pouvez utiliser SetUnhandledExceptionHandler pour créer un gestionnaire d'exceptions non capturé «global» - pour les exceptions SEH.
gbjbaanb

3
SEH est horrible et empêche également d'appeler des destructeurs C ++
paulm

6

Comme indiqué dans les autres réponses, C ++ peut prendre en charge des finallyfonctionnalités similaires. L'implémentation de cette fonctionnalité qui est probablement la plus proche de faire partie du langage standard est celle qui accompagne les C ++ Core Guidelines , un ensemble de meilleures pratiques pour l'utilisation du C ++ édité par Bjarne Stoustrup et Herb Sutter. Une implémentation definally fait partie de la bibliothèque de support des directives (GSL). Tout au long des directives, l'utilisation de finallyest recommandée lorsque vous traitez avec des interfaces à l'ancienne, et il a également sa propre directive, intitulée Utiliser un objet final_action pour exprimer le nettoyage si aucun descripteur de ressource approprié n'est disponible .

Ainsi, non seulement C ++ prend en charge finally, il est en fait recommandé de l'utiliser dans de nombreux cas d'utilisation courants.

Un exemple d'utilisation de l'implémentation GSL ressemblerait à:

#include <gsl/gsl_util.h>

void example()
{
    int handle = get_some_resource();
    auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });

    // Do a lot of stuff, return early and throw exceptions.
    // clean_that_resource will always get called.
}

L'implémentation et l'utilisation de GSL sont très similaires à celles de la réponse de Paolo.Bolzoni . Une différence est que l'objet créé par gsl::finally()n'a pas l' disable()appel. Si vous avez besoin de cette fonctionnalité (par exemple, pour retourner la ressource une fois qu'elle est assemblée et qu'aucune exception ne se produira), vous préférerez peut-être l'implémentation de Paolo. Sinon, l'utilisation de GSL est aussi proche que possible des fonctionnalités standardisées.


3

Pas vraiment, mais vous pouvez les émuler dans une certaine mesure, par exemple:

int * array = new int[10000000];
try {
  // Some code that can throw exceptions
  // ...
  throw std::exception();
  // ...
} catch (...) {
  // The finally-block (if an exception is thrown)
  delete[] array;
  // re-throw the exception.
  throw; 
}
// The finally-block (if no exception was thrown)
delete[] array;

Notez que le bloc finalement peut lui-même lever une exception avant que l'exception d'origine ne soit renvoyée, rejetant ainsi l'exception d'origine. C'est exactement le même comportement que dans un bloc Java finalement. De plus, vous ne pouvez pas utiliser returnà l'intérieur des blocs try & catch.


3
Je suis content que vous ayez mentionné le bloc enfin pourrait jeter; c'est la chose que la plupart des réponses «utiliser RAII» semblent ignorer. Pour éviter d'avoir à écrire le bloc enfin deux fois, vous pourriez faire quelque chose commestd::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
sethobrien

1
C'est tout ce que je voulais savoir! Pourquoi aucune des autres réponses n'a expliqué qu'un catch (...) + lancer vide; fonctionne presque comme un bloc enfin? Parfois, vous en avez juste besoin.
VinGarcia

La solution que j'ai fournie dans ma réponse ( stackoverflow.com/a/38701485/566849 ) devrait permettre de lever des exceptions depuis l'intérieur du finallybloc.
Fabio A.

3

J'ai créé une finallymacro qui peut être utilisée presque comme ¹ le finallymot - clé en Java; il utilise std::exception_ptret les amis, les fonctions lambda et std::promise, donc il nécessite C++11ou au-dessus; il utilise également l' extension de l' expression composée GCC, qui est également prise en charge par clang.

AVERTISSEMENT : une version antérieure de cette réponse utilisait une implémentation différente du concept avec de nombreuses autres limitations.

Définissons d'abord une classe d'assistance.

#include <future>

template <typename Fun>
class FinallyHelper {
    template <typename T> struct TypeWrapper {};
    using Return = typename std::result_of<Fun()>::type;

public:    
    FinallyHelper(Fun body) {
        try {
            execute(TypeWrapper<Return>(), body);
        }
        catch(...) {
            m_promise.set_exception(std::current_exception());
        }
    }

    Return get() {
        return m_promise.get_future().get();
    }

private:
    template <typename T>
    void execute(T, Fun body) {
        m_promise.set_value(body());
    }

    void execute(TypeWrapper<void>, Fun body) {
        body();
    }

    std::promise<Return> m_promise;
};

template <typename Fun>
FinallyHelper<Fun> make_finally_helper(Fun body) {
    return FinallyHelper<Fun>(body);
}

Ensuite, il y a la macro réelle.

#define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try 
#define finally });                         \
        true;                               \
        ({return __finally_helper.get();})) \
/***/

Il peut être utilisé comme ceci:

void test() {
    try_with_finally {
        raise_exception();
    }    

    catch(const my_exception1&) {
        /*...*/
    }

    catch(const my_exception2&) {
        /*...*/
    }

    finally {
        clean_it_all_up();
    }    
}

L'utilisation de std::promiserend très facile à implémenter, mais il introduit probablement aussi un peu de surcharge inutile qui pourrait être évitée en réimplémentant uniquement les fonctionnalités nécessaires à partir de std::promise.


¹ CAVEAT: il y a quelques choses qui ne fonctionnent pas comme la version java de finally. Du haut de ma tête:

  1. il est impossible de rompre une boucle externe avec la breakdéclaration à partir du tryet des catch()blocs de, car ils vivent dans une fonction lambda;
  2. il doit y avoir au moins un catch()bloc après try: c'est une exigence C ++;
  3. si la fonction a une valeur de retour autre que void mais qu'il n'y a pas de retour dans les blocs tryet catch()'s, la compilation échouera car la finallymacro se développera en code qui voudra retourner a void. Cela pourrait être, euh, un vide en ayant une finally_noreturnsorte de macro.

Dans l'ensemble, je ne sais pas si j'utiliserais ce truc moi-même, mais c'était amusant de jouer avec. :)


Oui, c'était juste un hack rapide, mais si le programmeur sait ce qu'il fait, cela pourrait néanmoins être utile.
Fabio A.

@MarkLakata, j'ai mis à jour le message avec une meilleure implémentation qui prend en charge la levée des exceptions et des retours.
Fabio A.

Cela semble bon. Vous pouvez vous débarrasser de Caveat 2 en insérant simplement un catch(xxx) {}bloc impossible au début de la finallymacro, où xxx est un type faux uniquement dans le but d'avoir au moins un bloc catch.
Mark Lakata

@MarkLakata, j'y ai pensé aussi, mais cela rendrait son utilisation impossible catch(...), non?
Fabio A.

Je ne pense pas. Créez simplement un type obscur xxxdans un espace de noms privé qui ne sera jamais utilisé.
Mark Lakata

2

J'ai un cas d'utilisation où je pense que finally devrait être une partie parfaitement acceptable du langage C ++ 11, car je pense qu'il est plus facile à lire d'un point de vue de flux. Mon cas d'utilisation est une chaîne de threads consommateur / producteur, où une sentinelle nullptrest envoyée à la fin de la course pour fermer tous les threads.

Si C ++ le supportait, vous voudriez que votre code ressemble à ceci:

    extern Queue downstream, upstream;

    int Example()
    {
        try
        {
           while(!ExitRequested())
           {
             X* x = upstream.pop();
             if (!x) break;
             x->doSomething();
             downstream.push(x);
           } 
        }
        finally { 
            downstream.push(nullptr);
        }
    }

Je pense que c'est plus logique que de mettre votre déclaration finally au début de la boucle, car cela se produit après la sortie de la boucle ... mais c'est un vœu pieux car nous ne pouvons pas le faire en C ++. Notez que la file d'attente downstreamest connectée à un autre thread, vous ne pouvez donc pas mettre la sentinelle push(nullptr)dans le destructeur de downstreamcar elle ne peut pas être détruite à ce stade ... elle doit rester en vie jusqu'à ce que l'autre thread reçoive lenullptr .

Voici donc comment utiliser une classe RAII avec lambda pour faire de même:

    class Finally
    {
    public:

        Finally(std::function<void(void)> callback) : callback_(callback)
        {
        }
        ~Finally()
        {
            callback_();
        }
        std::function<void(void)> callback_;
    };

et voici comment vous l'utilisez:

    extern Queue downstream, upstream;

    int Example()
    {
        Finally atEnd([](){ 
           downstream.push(nullptr);
        });
        while(!ExitRequested())
        {
           X* x = upstream.pop();
           if (!x) break;
           x->doSomething();
           downstream.push(x);
        }
    }

Bonjour, je crois que ma réponse ci-dessus ( stackoverflow.com/a/38701485/566849 ) répond pleinement à vos exigences.
Fabio A.

1

Comme beaucoup de gens l'ont dit, la solution consiste à utiliser les fonctionnalités C ++ 11 pour éviter enfin les blocages. L'une des fonctionnalités est unique_ptr.

Voici la réponse de Méphane écrite en utilisant des modèles RAII.

#include <vector>
#include <memory>
#include <list>
using namespace std;

class Foo
{
 ...
};

void DoStuff(vector<string> input)
{
    list<unique_ptr<Foo> > myList;

    for (int i = 0; i < input.size(); ++i)
    {
      myList.push_back(unique_ptr<Foo>(new Foo(input[i])));
    }

    DoSomeStuff(myList);
}

Une autre introduction à l'utilisation de unique_ptr avec les conteneurs de bibliothèque standard C ++ est ici


0

Je voudrais proposer une alternative.

Si vous voulez enfin que le bloc soit toujours appelé, placez-le juste après le dernier bloc catch (qui devrait probablement être catch( ... )pour intercepter une exception inconnue)

try{
   // something that might throw exception
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called always,
//don't forget that it might throw some exception too
doSomeCleanUp(); 

Si vous voulez enfin bloquer comme dernière chose à faire quand une exception est levée, vous pouvez utiliser une variable locale booléenne - avant de l'exécuter, définissez-la sur false et placez la véritable affectation à la toute fin du bloc try, puis après le blocage du bloc, vérifiez la variable valeur:

bool generalAppState = false;
try{
   // something that might throw exception

   //the very end of try block:
   generalAppState = true;
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called only when exception was thrown,
//don't forget that it might throw some exception too
if( !generalAppState ){
   doSomeCleanUpOfDirtyEnd();
}

//final code to be called only when no exception is thrown
//don't forget that it might throw some exception too
else{
   cleanEnd();
}

Cela ne fonctionne pas, car le but d'un bloc finally est d'effectuer un nettoyage même lorsque le code doit autoriser une exception à quitter le bloc de code. Considérez: `essayez {// des trucs qui pourraient lancer" B "} catch (A & a) {} enfin {// si C ++ l'avait ... // des trucs qui devaient arriver, même si" B "était lancé. } // ne s'exécutera pas si "B" est lancé. `À mon humble avis, le point des exceptions est de réduire le code de gestion des erreurs, donc les blocs catch, partout où un lancer pourrait se produire, sont contre-productifs. C'est pourquoi RAII aide: si elles sont appliquées généreusement, les exceptions comptent le plus dans les couches supérieures et inférieures.
Burlyearly

1
@burlyearly bien que votre opinion ne soit pas sainte, je comprends, mais en C ++ ce n'est pas une telle chose, vous devez donc considérer cela comme une couche supérieure qui émule ce comportement.
jave.web

DOWNVOTE = S'IL VOUS PLAÎT COMMENTER :)
jave.web

0

Je pense également que RIIA n'est pas un remplacement pleinement utile pour la gestion des exceptions et pour avoir enfin. BTW, je pense aussi que RIIA est un mauvais nom tout autour. J'appelle ces types de concierges de classe et je les utilise beaucoup. 95% du temps, ils n'initialisent ni n'acquièrent de ressources, ils appliquent certains changements sur une base limitée ou prennent quelque chose de déjà configuré et s'assurent qu'ils sont détruits. Ceci étant le nom officiel du modèle obsédé par Internet, je suis abusé pour avoir même suggéré que mon nom pourrait être meilleur.

Je ne pense pas qu'il soit raisonnable d'exiger que chaque configuration compliquée d'une liste de choses ad hoc doive avoir une classe écrite pour la contenir afin d'éviter des complications lors du nettoyage de tout en ayant besoin d'attraper plusieurs types d'exception en cas de problème dans le processus. Cela conduirait à de nombreuses classes ad hoc qui ne seraient tout simplement pas nécessaires autrement.

Oui, c'est bien pour les classes conçues pour gérer une ressource particulière, ou pour les classes génériques conçues pour gérer un ensemble de ressources similaires. Mais, même si toutes les choses impliquées ont de tels wrappers, la coordination du nettoyage peut ne pas être une simple invocation dans l'ordre inverse des destructeurs.

Je pense qu'il est parfaitement logique pour C ++ d'avoir enfin un. Je veux dire, jeez, tellement de morceaux ont été collés dessus au cours des dernières décennies qu'il semble que des gens étranges deviendraient soudainement conservateurs sur quelque chose comme finalement ce qui pourrait être très utile et probablement rien de plus compliqué que d'autres choses qui ont été ajouté (bien que ce soit juste une supposition de ma part.)


-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}

35
Idiome mignon, mais ce n'est pas tout à fait la même chose. retourner dans le bloc try ou catch ne passera pas par votre code «finalement:».
Edward KMETT

10
Cela vaut la peine de garder cette mauvaise réponse (avec 0 note), car Edward Kmett soulève une distinction très importante.
Mark Lakata

12
Encore plus gros défaut (IMO): ce code mange toutes les exceptions, ce qui finallyne fait pas.
Ben Voigt
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.