36 votes

Comment gérer les constructeurs qui doivent acquérir plusieurs ressources de manière exceptionnellement sécurisée

J'ai obtenu un non-trivial type qui possède de multiples ressources. Comment puis-je construire une exception sécurité?

Par exemple, voici une démo de la classe X qui est titulaire d'un tableau des A:

#include "A.h"

class X
{
    unsigned size_ = 0;
    A* data_ = nullptr;

public:
    ~X()
    {
        for (auto p = data_; p < data_ + size_; ++p)
            p->~A();
        ::operator delete(data_);
    }

    X() = default;
    // ...
};

Maintenant, la réponse la plus évidente de cette classe est d' utiliser std::vector<A>. Et que de bons conseils. Mais X n'est qu'un stand-in pour les scénarios plus complexes où l' X doit posséder de multiples ressources et il n'est pas commode d'utiliser les bons conseils de "utiliser les std::lib." J'ai choisi de communiquer sur la question avec cette structure de données simplement parce qu'il est familier.

Pour être en cristal clair: Si vous pouvez concevoir votre X telle qu'un défaut de paiement ~X() correctement nettoie tout ("la règle du zéro"), ou si ~X() n'a qu'à sortir une seule ressource, alors qui est le meilleur. Cependant, il y a des moments dans la vie réelle lorsqu' ~X() a affaire à de multiples ressources, et cette question répond à ces circonstances.

Donc, ce type a déjà une bonne destructeur, et un bon constructeur par défaut. Ma question est axé sur une non-trivial constructeur qui prend deux As', alloue de l'espace pour eux, et les constructions eux:

X::X(const A& x, const A& y)
    : size_{2}
    , data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
    ::new(data_) A{x};
    ::new(data_ + 1) A{y};
}

J'ai entièrement instrumenté de la classe de test A et si aucune exception n'est levée à partir de ce constructeur, il fonctionne parfaitement. Par exemple avec ce test pilote:

int
main()
{
    A a1{1}, a2{2};
    try
    {
        std::cout << "Begin\n";
        X x{a1, a2};
        std::cout << "End\n";
    }
    catch (...)
    {
        std::cout << "Exceptional End\n";
    }
}

La sortie est:

A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
A(A const& a): 2
End
~A(1)
~A(2)
~A(2)
~A(1)

J'ai 4 constructions, et 4 de destructions, et de chaque destruction correspond à un constructeur. Tout est bien.

Toutefois, si le constructeur de copie d' A{2} déclenche une exception, j'obtiens ce résultat:

A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
Exceptional End
~A(2)
~A(1)

Maintenant, j'ai 3 constructions mais seulement 2 des destructions. L' A résultant de l' A(A const& a): 1 a été coulé!

Une façon de résoudre ce problème est de la dentelle le constructeur try/catch. Toutefois, cette approche n'est pas évolutif. Après chaque seul d'allocation de ressources, j'ai besoin d'encore un autre imbriquée try/catch pour tester la prochaine allocation des ressources et de libérer ce qui a déjà été alloués. Détient le nez:

X(const A& x, const A& y)
    : size_{2}
    , data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
    try
    {
        ::new(data_) A{x};
        try
        {
            ::new(data_ + 1) A{y};
        }
        catch (...)
        {
            data_->~A();
            throw;
        }
    }
    catch (...)
    {
        ::operator delete(data_);
        throw;
    }
}

Cette correctement les résultats:

A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
~A(1)
Exceptional End
~A(2)
~A(1)

Mais c'est moche! Que faire si il y a 4 ressources? Ou 400?! Que faire si le nombre de ressources est pas connu au moment de la compilation?!

Est-il un meilleur moyen?

41voto

Howard Hinnant Points 59526

Est-il un meilleur moyen?

OUI

C++11 fournit une nouvelle fonctionnalité appelée déléguer les constructeurs qui s'occupe de cette situation très gracieusement. Mais c'est un peu subtile.

Le problème avec de lever des exceptions dans les constructeurs est de réaliser que le destructeur de l'objet que vous êtes la construction n'est pas exécutée jusqu'à ce que le constructeur est terminée. Bien que les destructeurs de sous-objets (les bases et les membres) sera exécuté si une exception est levée, dès que ces sous-objets sont entièrement construits.

La clé ici est de construire X avant de commencer à ajouter des ressources, et ensuite ajouter des ressources à un à un, en gardant l' X dans un état valide à mesure que vous ajoutez chaque ressource. Une fois l' X est entièrement construit, ~X() va nettoyer tous les gâchis que vous ajoutez des ressources. Avant C++11 ce qui pourrait ressembler à:

X x;  // no resources
x.push_back(A(1));  // add a resource
x.push_back(A(2));  // add a resource
// ...

Mais en C++11, vous pouvez écrire le multi-ressources-acquizition constructeur comme ceci:

X(const A& x, const A& y)
    : X{}
{
    data_ = static_cast<A*>(::operator new (2*sizeof(A)));
    ::new(data_) A{x};
    ++size_;
    ::new(data_ + 1) A{y};
    ++size_;
}

C'est un peu comme l'écriture de code complètement ignorant de l'exception de sécurité. La différence est cette ligne:

    : X{}

Cela dit: Construisez-moi un défaut X. Après cette construction, *this est entièrement construit et si une exception est levée dans les opérations suivantes, ~X() obtient exécuter. C'est révolutionnaire!

Notez que dans ce cas, un défaut construits X acquiert pas de ressources. En effet, il est même implicitement noexcept. De sorte que la partie ne sera pas levée. Et il définit *this valable X qui est titulaire d'un tableau de taille 0. ~X() sait comment faire face à cet état.

Maintenant, ajoutez les ressources de la mémoire non initialisée. Si ça en jette, vous avez encore un défaut construits X et ~X() correctement traite que en ne faisant rien.

Maintenant, ajoutez la deuxième ressource: la construction d'Un exemplaire de l' x. Si ça en jette, ~X() sera toujours de libérer l' data_ de la mémoire tampon, mais sans courir de l' ~A().

Si la deuxième ressource réussit, définissez l' X à un état valide par incrémentation size_ qui est un noexcept de l'opération. Si quoi que ce soit après ce lève, ~X() correctement nettoyer un tampon de longueur 1.

Essayez maintenant de la troisième ressource: la construction d'Un exemplaire de l' y. Si la construction qui en jette, ~X() correctement nettoyer votre tampon de longueur 1. Si il ne jette pas, informez - *this qu'il possède maintenant un tampon de longueur 2.

L'utilisation de cette technique n'a pas besoin X à la valeur par défaut constructible. Par exemple, le constructeur par défaut pourrait être privé. Ou vous pouvez utiliser un autre constructeur qui met X dans une démunis état:

: X{moved_from_tag{}}

En C++11, il est généralement une bonne idée si votre X peut avoir une démunis état que cela vous permet d'avoir un noexcept constructeur de déplacement qui est livré avec toutes sortes de bonté (et est le sujet d'un autre post).

C++11 déléguer les constructeurs est une très bonne (évolutive) technique pour la rédaction d'exception sûr des constructeurs, tant que vous disposez d'une ressource-moins d'etat à construire au début (par exemple, un noexcept constructeur par défaut).

Oui, il y a des façons de le faire en C++98/03, mais ils ne sont pas aussi jolie. Vous devez créer une mise en œuvre détaillée de la classe de base de l' X qui contient la destruction de la logique de l' X, mais pas la construction de la logique. Été là, fait cela, j'aime déléguer des constructeurs.

7voto

Victor Savu Points 562

Je pense que le problème découle d'une violation du Principe de Responsabilité Unique: la Classe X a pour traiter de la gestion de la durée de vie de plusieurs objets (et c'est probablement même pas de sa responsabilité principale).

Le destructeur d'une classe ne devrait libérer les ressources que la classe a directement acquis. Si la classe est juste un composite (c'est à dire une instance de la classe possède des instances d'autres classes), l'idéal est de s'appuyer sur la gestion automatique de la mémoire (via RAII) et il suffit d'utiliser le destructeur par défaut. Si la classe a à gérer certaines ressources spécialisées manuellement (par exemple, s'ouvre un descripteur de fichier ou d'une connexion, acquiert un verrou ou alloue de la mémoire), je recommanderais d'affacturage la responsabilité de la gestion de ces ressources à une classe dédiée à cet effet, puis à l'aide des instances de cette classe en tant que membres.

À l'aide de la bibliothèque de modèles standard serait, en effet, aider, car il contient les structures de données (tels que les pointeurs intelligents et std::vector<T>) exclusivement de gérer ce problème. Ils sont également compossible, de sorte que même si votre X doit contenir plusieurs instances d'objets complexes sur des stratégies d'acquisition des ressources, le problème de la gestion des ressources dans une exception manière sécuritaire est résolu à la fois pour chaque membre ainsi que pour le contenant composite de classe X.

2voto

Remy Lebeau Points 130112

En C ++11, essayez peut-être quelque chose comme ceci:

 #include "A.h"
#include <vector>

class X
{
    std::vector<A> data_;

public:
    X() = default;

    X(const A& x, const A& y)
        : data_{x, y}
    {
    }

    // ...
};
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X