225 votes

La nouvelle syntaxe "= default" en C++11

Je ne comprends pas pourquoi je ferais jamais ça :

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

Pourquoi ne pas simplement dire :

S() {} // au lieu de S() = default;

pourquoi introduire une nouvelle syntaxe pour cela ?

41 votes

Pinailler : default n'est pas un nouveau mot-clé, c'est simplement une nouvelle utilisation d'un mot-clé déjà réservé.

1 votes

1 votes

Peut-être Cette question pourra vous aider.

199voto

Joseph Mansfield Points 59346

Un constructeur par défaut defaultisé est spécifiquement défini comme étant le même qu'un constructeur par défaut défini par l'utilisateur sans liste d'initialisation et avec une déclaration de bloc vide.

§12.1/6 [class.ctor] Un constructeur par défaut qui est defaultisé et n'est pas défini comme effacé est implicitement défini lorsqu'il est odr-used pour créer un objet de son type de classe ou lorsqu'il est explicitement defaultisé après sa première déclaration. Le constructeur par défaut implicitement défini effectue l'ensemble des initialisations de la classe qui seraient effectuées par un constructeur par défaut écrit par l'utilisateur pour cette classe sans ctor-initializer (12.6.2) et une déclaration de bloc vide. [...]

Cependant, bien que les deux constructeurs se comportent de la même manière, fournir une implémentation vide affecte certaines propriétés de la classe. Donner un constructeur défini par l'utilisateur, même s'il ne fait rien, rend le type non un agrégat et également non trivial. Si vous voulez que votre classe soit un agrégat ou un type trivial (ou par transitivité, un type POD), alors vous devez utiliser = default.

§8.5.1/1 [dcl.init.aggr] Un agrégat est un tableau ou une classe sans constructeurs fournis par l'utilisateur, [et...]

§12.1/5 [class.ctor] Un constructeur par défaut est trivial s'il n'est pas fourni par l'utilisateur et [...]

§9/6 [class] Une classe triviale est une classe qui a un constructeur par défaut trivial et [...]

Pour démontrer :

#include 

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial::value, "X devrait être trivial");
    static_assert(std::is_pod::value, "X devrait être POD");

    static_assert(!std::is_trivial::value, "Y ne devrait pas être trivial");
    static_assert(!std::is_pod::value, "Y ne devrait pas être POD");
}

De plus, définir explicitement un constructeur par default le rendra constexpr si le constructeur implicite l'aurait été et lui donnera également la même spécification d'exception que le constructeur implicite aurait eu. Dans le cas que vous avez donné, le constructeur implicite n'aurait pas été constexpr (car il aurait laissé un membre de données non initialisé) et aurait également une spécification d'exception vide, donc il n'y a pas de différence. Mais oui, dans le cas général, vous pourriez spécifier manuellement constexpr et la spécification de l'exception pour correspondre au constructeur implicite.

Utiliser = default apporte une certaine uniformité, car cela peut également être utilisé avec les constructeurs de copie/déplacement et les destructeurs. Un constructeur de copie vide, par exemple, ne fera pas la même chose qu'un constructeur de copie defaultisé (qui effectuera une copie membre par membre de ses membres). Utiliser la syntaxe = default (ou = delete) uniformément pour chacune de ces fonctions membres spéciales rend votre code plus facile à lire en déclarant explicitement votre intention.

0 votes

Presque. 12.1/6 : "Si ce constructeur par défaut écrit par l'utilisateur satisfait aux exigences d'un constructeur constexpr (7.1.5), le constructeur par défaut implicitement défini est constexpr."

0 votes

En réalité, 8.4.2/2 est plus informatif : "Si une fonction est explicitement définie par défaut lors de sa première déclaration, (a) elle est implicitement considérée comme étant constexpr si la déclaration implicite le serait, (b) elle est implicitement considérée comme ayant la même spécification d'exception que si elle avait été implicitement déclarée (15.4), ..." Cela ne fait pas de différence dans ce cas spécifique, mais en général foo() = default; a un léger avantage sur foo() {}.

5 votes

Vous dites qu'il n'y a pas de différence, puis vous continuez à expliquer les différences?

34voto

Slavenskij Points 181

J'ai un exemple qui montrera la différence :

#include 

using namespace std;
class A 
{
public:
    int x;
    A(){}
};

class B 
{
public:
    int x;
    B()=default;
};

int main() 
{ 
    int x = 5;
    new(&x)A(); // Appel du constructeur vide, qui ne fait rien
    cout << x << endl;
    new(&x)B; // Appel du constructeur par défaut
    cout << x << endl;
    new(&x)B(); // Appel du constructeur par défaut + Initialisation de la valeur
    cout << x << endl;
    return 0; 
} 

Sortie :

5
5
0

Comme nous pouvons le voir, l'appel du constructeur vide A() n'initialise pas les membres, tandis que B() le fait.

53 votes

Veuillez expliquer cette syntaxe -> nouveau(&x)A();

29 votes

Nous créons un nouvel objet dans la mémoire à partir de l'adresse de la variable x (au lieu d'une nouvelle allocation de mémoire). Cette syntaxe est utilisée pour créer un objet dans une mémoire pré-allouée. Dans notre cas, la taille de B = la taille de int, donc new(&x)A() créera un nouvel objet à la place de la variable x.

1 votes

Je reçois des résultats différents avec gcc 8.3 : ideone.com/XouXux

10voto

n2210 fournit quelques raisons :

La gestion des défauts comporte plusieurs problèmes :

  • Les définitions de constructeurs sont liées ; la déclaration de tout constructeur supprime le constructeur par défaut.
  • Le destructeur par défaut est inapproprié pour les classes polymorphiques, exigeant une définition explicite.
  • Une fois un défaut supprimé, il n'y a aucun moyen de le rétablir.
  • Les implémentations par défaut sont souvent plus efficaces que les implémentations manuelles spécifiées.
  • Les implémentations non par défaut sont non triviales, ce qui affecte la sémantique de type, par exemple rend un type non-POD.
  • Il n'y a aucun moyen d'interdire une fonction membre spéciale ou un opérateur global sans déclarer un substitut (non trivial).

type::type() = default;
type::type() { x = 3; }

Dans certains cas, le corps de la classe peut changer sans nécessiter un changement dans la définition de la fonction membre car le défaut change avec la déclaration de membres supplémentaires.

Consulter Règle-des-Trois devient Règle-des-Cinq avec C++11? :

Notez que le constructeur de déplacement et l'opérateur d'affectation de déplacement ne seront pas générés pour une classe qui déclare explicitement l'une des autres fonctions membres spéciales, que le constructeur de copie et l'opérateur d'affectation de copie ne seront pas générés pour une classe qui déclare explicitement un constructeur de déplacement ou un opérateur d'affectation de déplacement, et qu'une classe avec un destructeur explicitement déclaré et un constructeur de copie définit implicitement ou opérateur d'affectation de copie implicitement défini est considéré comme obsolète

1 votes

Il y a des raisons d'avoir = default en général, plutôt que des raisons de faire = default sur un constructeur par rapport à faire { }.

0 votes

@JosephMansfield Certes, mais étant donné que {} était déjà une fonctionnalité du langage avant l'introduction de =default, ces raisons reposent implicitement sur la distinction (par exemple "il n'y a aucun moyen de ressusciter [un défaut supprimé]" implique que {} n'est pas équivalent au défaut).

10voto

Sean Middleditch Points 1050

Il s'agit de sémantique dans certains cas. Ce n'est pas très évident avec les constructeurs par défaut, mais cela devient évident avec d'autres fonctions membres générées par le compilateur.

Pour le constructeur par défaut, il aurait été possible de considérer tout constructeur par défaut avec un corps vide comme candidat à être un constructeur trivial, tout comme l'utilisation de =default. Après tout, les anciens constructeurs par défaut vides étaient du C++ légal.

struct S { 
  int a; 
  S() {} // C++ légal 
};

Que le compilateur comprenne ou non que ce constructeur est trivial est sans importance dans la plupart des cas en dehors des optimisations (manuelles ou celles du compilateur).

Cependant, cette tentative de traiter les corps de fonctions vides comme "par défaut" échoue complètement pour d'autres types de fonctions membres. Considérez le constructeur de copie :

struct S { 
  int a; 
  S() {}
  S(const S&) {} // légal, mais sémantiquement incorrect
};

Dans le cas ci-dessus, le constructeur de copie écrit avec un corps vide est maintenant incorrect. Il ne copie plus rien en réalité. C'est un ensemble de sémantiques très différentes de celles du constructeur de copie par défaut. Le comportement souhaité exige que vous écriviez du code :

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // corrigé
};

Même avec ce cas simple, cependant, il devient beaucoup plus contraignant pour le compilateur de vérifier que le constructeur de copie est identique à celui qu'il générerait lui-même ou de voir que le constructeur de copie est trivial (équivalent à un memcpy, essentiellement). Le compilateur devrait vérifier chaque expression d'initialisation de membre et s'assurer qu'elle est identique à l'expression d'accès au membre correspondant de la source et rien d'autre, s'assurer qu'aucun membre n'est laissé avec une construction par défaut non triviale, etc. C'est à l'envers par rapport au processus que le compilateur utiliserait pour vérifier que ses propres versions générées de cette fonction sont triviales.

Considérez ensuite l'opérateur d'affectation par copie qui peut devenir encore plus compliqué, surtout dans le cas non trivial. C'est beaucoup de code répétitif que vous ne voulez pas avoir à écrire pour de nombreuses classes, mais que vous êtes quand même obligé d'écrire en C++03 :

struct T { 
  std::shared_ptr b; 
  T(); // les définitions habituelles
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // pas vraiment nécessaire pour cet exemple
      b = src.b; // opération non triviale
    return *this;
};

C'est un cas simple, mais c'est déjà plus de code que vous souhaiteriez jamais être obligé d'écrire pour un type aussi simple que T (surtout une fois que nous ajoutons les opérations de déplacement). Nous ne pouvons pas nous fier au corps vide pour signifier "remplir les valeurs par défaut" car le corps vide est déjà parfaitement valide et a une signification claire. En fait, si le corps vide était utilisé pour signifier "remplir les valeurs par défaut", il n'y aurait aucun moyen de créer explicitement un constructeur de copie sans effet ou similaire.

C'est à nouveau une question de cohérence. Le corps vide signifie "ne rien faire" mais pour des choses comme les constructeurs de copie, vous ne voulez vraiment pas dire "ne rien faire" mais plutôt "faire toutes les choses que vous feriez normalement si elles n'étaient pas supprimées". D'où l'utilisation de =default. C'est nécessaire pour surmonter les fonctions membres générées par le compilateur supprimées telles que les constructeurs et les opérateurs d'affectation par copie. Il est alors tout simplement "évident" de le faire fonctionner également pour le constructeur par défaut.

Il aurait peut-être été intéressant de considérer les constructeurs par défaut avec des corps vides et des constructeurs de membres/bases triviaux également comme triviaux, tout comme avec =default afin de rendre le code plus optimal dans certains cas, mais la plupart du code de bas niveau se basant sur des constructeurs par défaut triviaux pour les optimisations dépend également de constructeurs de copie triviaux. Si vous devez aller "corriger" tous vos anciens constructeurs de copie, il n'est vraiment pas très difficile de devoir corriger également tous vos anciens constructeurs par défaut. C'est aussi beaucoup plus clair et évident d'utiliser =default pour indiquer vos intentions.

Il y a quelques autres choses que les fonctions membres générées par le compilateur feront que vous devrez explicitement modifier pour supporter, également. Le support de constexpr pour les constructeurs par défaut en est un exemple. C'est tout simplement plus simple mentalement d'utiliser =default que de devoir marquer les fonctions avec tous les autres mots-clés spéciaux et autres implicites de =default et c'était l'un des thèmes de C++11 : rendre le langage plus simple. Il a encore plein de défauts et de compromis de compatibilité arrière mais il est clair qu'il constitue un grand pas en avant par rapport à C++03 en termes de facilité d'utilisation.

0 votes

J'ai eu un problème où je m'attendais à ce que = default fasse a=0; et ce n'était pas le cas! J'ai dû l'abandonner au profit de : a(0). Je suis encore confus quant à l'utilité de = default, est-ce lié aux performances? Est-ce que quelque chose va se casser si je n'utilise pas simplement = default? J'ai essayé de lire toutes les réponses ici mais je suis nouveau dans certains aspects du C++ et j'ai beaucoup de mal à le comprendre.

0 votes

@AquariusPower : il ne s'agit pas "juste" de performance mais est également requis dans certains cas autour des exceptions et d'autres sémantiques. En effet, un opérateur par défaut peut être trivial mais un opérateur non par défaut ne peut jamais être trivial, et certains codes utiliseront des techniques de méta-programmation pour modifier le comportement des types avec des opérations non triviales. Votre exemple a=0 est dû au comportement des types triviaux, qui sont un sujet séparé (bien que lié).

0 votes

Est-ce que cela signifie qu'il est possible d'avoir = default et toujours accorder que a sera =0? d'une certaine façon? Pensez-vous que je pourrais créer une nouvelle question comme "comment avoir un constructeur = default et garantir que les champs seront correctement initialisés?", au fait j'avais le problème dans une struct et non dans une class, et l'application fonctionne correctement même sans utiliser = default, je peux ajouter une struct minimale à cette question si c'est une bonne idée :)

6voto

trozen Points 115

Il y a une différence significative lors de la création d'un objet via new T(). En cas de constructeur par défaut, une initialisation de l'agrégat par défaut se produira, initialisant toutes les valeurs des membres à des valeurs par défaut. Cela n'arrivera pas en cas de constructeur vide. (ne se produira pas non plus avec new T)

Considérez la classe suivante:

struct T {
    T() = default;
    T(int x, int c) : s(c) {
        for (int i = 0; i < s; i++) {
            d[i] = x;
        }
    }
    T(const T& o) {
        s = o.s;
        for (int i = 0; i < s; i++) {
            d[i] = o.d[i];
        }
    }
    void push(int x) { d[s++] = x; }
    int pop() { return d[--s]; }

private:
    int s = 0;
    int d[1<<20];
};

new T() initialisera tous les membres à zéro, y compris le tableau de 4 Mio (memset à 0 en cas de gcc). Ce n'est évidemment pas souhaité dans ce cas, définir un constructeur vide T() {} empêcherait cela.

En fait, je suis tombé une fois sur un tel cas, lorsque CLion a suggéré de remplacer T() {} par T() = default. Cela a entraîné une baisse significative des performances et des heures de débogage/benchmarking.

Je préfère donc utiliser un constructeur vide après tout, sauf si je veux vraiment pouvoir utiliser une initialisation d'agrégat.

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