Pourquoi C++11 a-t-il introduit les constructeurs délégataires?

Je ne comprends pas à quoi sert de déléguer des constructeurs. Tout simplement, que ne peut-on accomplir sans avoir délégué les constructeurs?

Il peut faire quelque chose de simple comme ceci

class M 
{
 int x, y;
 char *p;
public:
 M(int v) : x(v), y(0), p(new char [MAX]) {}
 M(): M(0) {cout<<"delegating ctor"<<endl;}
};

Mais je ne vois pas qu'il vaut la peine d'introduire une nouvelle fonctionnalité pour une chose si simple? Peut-être que je ne pouvais pas reconnaître le point important. Une idée?

22
demandé sur Rich L 2014-10-05 07:44:44

5 réponses

Déléguer les constructeurs empêche la duplication de code (et toutes les erreurs et les défauts possibles qui en découlent : entretien accru, diminution de la lisibilité...), ce qui est une bonne chose.

c'est aussi la seule façon de déléguer la liste d'initialisation (pour les initialisations des membres et des bases), c'est-à-dire que vous ne pouvez vraiment pas remplacer cette fonctionnalité en ayant un méthode pour votre constructeur.


Exemples:

1) initialisation commune à partir de N1986 proposition:

class X { 
 X( int, W& ); 
 Y y_; 
 Z z_; 
public: 
 X(); 
 X( int ); 
 X( W& ); 
}; 
X::X( int i, W& e ) : y_(i), z_(e) { /*Common Init*/ } 
X::X() : X( 42, 3.14 )             { SomePostInitialization(); } 
X::X( int i ) : X( i, 3.14 )       { OtherPostInitialization(); } 
X::X( W& w ) : X( 53, w )          { /* no post-init */ } 

2) Délégation avec à la fois le constructeur et le constructeur de copie, aussi à partir de N1986 proposition:

class FullName { 
 string firstName_; 
 string middleName_; 
 string lastName_; 

public: 
 FullName(string firstName, string middleName, string lastName); 
 FullName(string firstName, string lastName); 
 FullName(const FullName& name); 
}; 
FullName::FullName(string firstName, string middleName, string lastName) 
 : firstName_(firstName), middleName_(middleName), lastName_(lastName) 
{ 
 // ... 
} 
// delegating copy constructor 
FullName::FullName(const FullName& name) 
 : FullName(name.firstName_, name.middleName_, name.lastName_) 
{ 
 // ... 
} 
// delegating constructor 
FullName::FullName(string firstName, string lastName) 
 : FullName(firstName, "", lastName) 
{ 
 // ... 
} 

3)MSDN donne cet exemple, avec les constructeurs effectuant la validation des arguments (commentée, cette conception est discutable) :

class class_c {
public:
    int max;
    int min;
    int middle;

    class_c() {}
    class_c(int my_max) { 
        max = my_max > 0 ? my_max : 10; 
    }
    class_c(int my_max, int my_min) { 
        max = my_max > 0 ? my_max : 10;
        min = my_min > 0 && my_min < max ? my_min : 1;
    }
    class_c(int my_max, int my_min, int my_middle) {
        max = my_max > 0 ? my_max : 10;
        min = my_min > 0 && my_min < max ? my_min : 1;
        middle = my_middle < max && my_middle > min ? my_middle : 5;
    }
};

grâce à la délégation des constructeurs, il se réduit à:

class class_c {
public:
    int max;
    int min;
    int middle;

    class_c(int my_max) { 
        max = my_max > 0 ? my_max : 10; 
    }
    class_c(int my_max, int my_min) : class_c(my_max) { 
        min = my_min > 0 && my_min < max ? my_min : 1;
    }
    class_c(int my_max, int my_min, int my_middle) : class_c (my_max, my_min){
        middle = my_middle < max && my_middle > min ? my_middle : 5;
}
};

Liens:

42
répondu quantdev 2014-10-10 12:43:44

en plus de l'excellente réponse de quantdev (que j'ai rappelée), j'ai voulu également démontrer les problèmes de sécurité d'exception de déléguer des constructeurs pour les types qui doivent explicitement acquérir des ressources multiples dans un constructeur, et explicitement disposer de ressources multiples dans son destructeur.

à titre d'exemple, j'utiliserai des pointeurs bruts simples. Notez que cet exemple n'est pas très motivant car l'utilisation de pointeurs intelligents sur des pointeurs bruts résoudra le problème plus proprement que déléguer des constructeurs. Mais l'exemple est simple. Il existe encore des exemples plus complexes qui ne sont pas résolus par des pointeurs intelligents.

Considérons deux classes X et Y, qui sont des classes normales, sauf que j'ai décoré leurs membres spéciaux de déclarations imprimées pour qu'on puisse les voir, et Y a un constructeur de copie qui pourrait lancer (dans notre exemple simple, il lance toujours juste pour une démonstration):

#include <iostream>

class X
{
public:
    X()
    {
        std::cout << "X()\n";
    }

    ~X()
    {
        std::cout << "~X()\n";
    }

    X(const X&)
    {
        std::cout << "X(const&)\n";
    }

    X& operator=(const X&) = delete;
};

class Y
{
public:
    Y()
    {
        std::cout << "Y()\n";
    }

    ~Y()
    {
        std::cout << "~Y()\n";
    }

    Y(const Y&)
    {
        throw 1;
    }

    Y& operator=(const Y&) = delete;
};

maintenant le la classe de démonstration est Z qui tient un pointeur géré manuellement vers un X et Y, juste pour créer "plusieurs gérés manuellement ressources."

class Z
{
    X* x_ptr;
    Y* y_ptr;
public:
    Z()
        : x_ptr(nullptr)
        , y_ptr(nullptr)
    {}

    ~Z()
    {
        delete x_ptr;
        delete y_ptr;
    }

    Z(const X& x, const Y& y)
        : x_ptr(new X(x))
        , y_ptr(new Y(y))
        {}
};

Z(const X& x, const Y& y) le constructeur tel qu'il est n'est pas exceptionnellement sûr. Pour le démontrer:

int
main()
{
    try
    {
        Z z{X{}, Y{}};
    }
    catch (...)
    {
    }
}

les résultats:

X()
Y()
X(const&)
~Y()
~X()

X a été construit deux fois, mais détruit une seule fois. Il y a une fuite de mémoire. Il y a plusieurs façons de rendre ce constructeur sûr, une seule façon est:

Z(const X& x, const Y& y)
    : x_ptr(new X(x))
    , y_ptr(nullptr)
{
    try
    {
        y_ptr = new Y(y);
    }
    catch (...)
    {
        delete x_ptr;
        throw;
    }
}

le programme exemple affiche maintenant correctement:

X()
Y()
X(const&)
~X()
~Y()
~X()

cependant on peut facilement voir que comme vous ajoutez des ressources gérées à Z, cela devient vite fastidieux. Ce problème est résolu très élégant, en déléguant des constructeurs:

Z(const X& x, const Y& y)
    : Z()
{
    x_ptr = new X(x);
    y_ptr = new Y(y);
}

ce constructeur délègue d'abord au constructeur par défaut qui ne fait rien d'autre que de mettre la classe dans un état valide, sans ressources. Une fois que le constructeur par défaut a terminé, Z est maintenant considéré comme entièrement construite. Donc, si quelque chose dans le corps de ce constructeur lève, ~Z() exécute maintenant (contrairement aux implémentations d'exemple précédentes de Z(const X& x, const Y& y). Et ~Z() nettoie correctement les ressources qui ont déjà été construites (et ignore celles qui ne l'ont pas été).

si vous devez écrire une classe qui gère plusieurs ressources dans son destructeur, et pour quelque raison que ce soit, vous ne pouvez pas utiliser d'autres objets pour gérer ces ressources (par exemple unique_ptr), je nous recommandons fortement cet idiome pour gérer la sécurité d'exception.

mise à Jour

peut-être un exemple plus motivant est une classe de conteneur personnalisé (la std::lib ne fournit pas conteneurs).

Votre classe de conteneur pourrait ressembler à:

template <class T>
class my_container
{
    // ...
public:
    ~my_container() {clear();}
    my_container();  // create empty (resource-less) state
    template <class Iterator> my_container(Iterator first, Iterator last);
    // ...
};

Une façon de mettre en œuvre les états-modèle constructeur est:

template <class T>
template <class Iterator>
my_container<T>::my_container(Iterator first, Iterator last)
{
    // create empty (resource-less) state
    // ...
    try
    {
        for (; first != last; ++first)
            insert(*first);
    }
    catch (...)
    {
        clear();
        throw;
    }
}

Mais voici comment je ferais:

template <class T>
template <class Iterator>
my_container<T>::my_container(Iterator first, Iterator last)
    : my_container() // create empty (resource-less) state
{
    for (; first != last; ++first)
        insert(*first);
}

si quelqu'un est en code revue appelé la dernière mauvaise pratique, je voudrais aller au tapis sur celui-ci.

17
répondu Howard Hinnant 2014-10-05 23:38:39

une utilisation clé de la délégation de constructeurs qui ne réduit pas simplement la duplication de code est d'obtenir des paquets de paramètres de modèle supplémentaires, en particulier une séquence d'indices entiers, nécessaires pour spécifier un initialiseur de membre:

Par exemple:

struct constant_t;

template <class T, size_t N>
struct Array {
    T data[N];
    template <size_t... Is>
    constexpr Array(constant_t, T const &value, std::index_sequence<Is...>)
        : data { (Is,value)... }
    {}

    constexpr Array(constant_t, T const &value)
        : Array(constant_t{}, value, std::make_index_sequence<N>{})
    {}
};

de cette façon, nous pouvons définir un constructeur qui initialise le tableau à une valeur constante sans initialiser chaque élément par défaut. La seule autre façon d'y parvenir, pour autant que je sache, consisterait à membre de données dans une classe de base.

bien sûr, une meilleure prise en charge de la langue pour les paquets de paramètres de template pourrait rendre cela inutile.

6
répondu jbms 2014-10-08 23:58:54

j'ai décrit une autre utilisation de la délégation des constructeurs en surcharge # 113 qui simplifie les solutions décrites dans logique complexe dans la liste des Initialisateurs membres en surcharge # 112.

contrairement au code à l'intérieur d'un corps de fonction, lorsque vous écrivez les initialiseurs de membres d'un constructeur, vous ne pouvez pas créer une variable locale pour stocker un résultat intermédiaire qui est nécessaire à plus d'un des membres.

considérer un constructeur comme ceci:

double some_expensive_calculation(double d);

bar::bar(double d)
: x_(cos(some_expensive_calculation(d))), y_(sin(some_expensive_calculation(d)))
{ }

nous voulons éviter d'effectuer le calcul coûteux deux fois (et dans le contexte du problème original décrit par Cassio, une classe de base veut aussi le résultat du calcul, donc vous ne pouvez pas simplement attribuer à x_ et y_ dans le corps du constructeur).

Le truc que j'ai décrit est de calculer le résultat intermédiaire et de déléguer à un autre constructeur qui utilise ce résultat:

class bar {
  struct tag { };
  ...
  bar(double result, tag);

public:
  bar(double d);
};

bar::bar(double d)
: bar(some_expensive_calculation(d), tag{})
{ }

bar::bar(double result, tag)
: x_(cos(result)), y_(sin(result))
{ }
2
répondu Jonathan Wakely 2014-10-10 13:00:09

il me semble qu'il vaut la peine de mentionner qu'il a parfois été suggéré que la duplication de code entre plusieurs constructeurs peut être allégée en refactorisant le code commun en une fonction init-privée. Le problème avec cela est que si la classe a des amis, ceux-ci peuvent invoquer l'init plusieurs fois, et il n'est pas censé être invoqué plusieurs fois. La délégation des constructeurs empêche de tels problèmes du fait que les constructeurs ne peuvent pas exécuter après que l'objet a été initialisé déjà.

2
répondu Ville Voutilainen 2014-10-11 13:05:15