La nouvelle syntaxe «= default» en C ++ 11

136

Je ne comprends pas pourquoi je ferais ça:

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

Pourquoi ne pas simplement dire:

S() {} // instead of S() = default;

pourquoi introduire une nouvelle syntaxe pour cela?

user3111311
la source
30
Nitpick: defaultn'est pas un nouveau mot-clé, c'est simplement une nouvelle utilisation d'un mot-clé déjà réservé.
Double
Mey être Cette question pourrait vous aider.
FreeNickname
7
En plus des autres réponses, je dirais également que «= default;» est plus auto-documenté.
Mark
En relation: stackoverflow.com/questions/13576055/…
Gabriel Staples

Réponses:

136

Un constructeur par défaut par défaut est spécifiquement défini comme étant identique à un constructeur par défaut défini par l'utilisateur sans liste d'initialisation et une instruction composée vide.

§12.1 / 6 [class.ctor] Un constructeur par défaut qui est défini par défaut et non défini comme supprimé est implicitement défini lorsqu'il est utilisé par odr pour créer un objet de son type de classe ou lorsqu'il est explicitement défini par défaut après sa première déclaration. Le constructeur implicitement défini par défaut exécute 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 initialiseur ctor (12.6.2) et une instruction composée 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 pas non plus trivial . Si vous voulez que votre classe soit un type agrégé ou trivial (ou par transitivité, un type POD), vous devez utiliser = default.

§8.5.1 / 1 [dcl.init.aggr] Un agrégat est un tableau ou une classe sans constructeur fourni 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 [classe] Une classe triviale est une classe qui a un constructeur par défaut trivial et [...]

Démontrer:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial<X>::value, "X should be trivial");
    static_assert(std::is_pod<X>::value, "X should be POD");
    
    static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
    static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

De plus, le fait de définir explicitement un constructeur par défaut le rendra constexprsi le constructeur implicite 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 laisserait un membre de données non initialisé) et il aurait également une spécification d'exception vide, il n'y a donc aucune différence. Mais oui, dans le cas général, vous pouvez spécifier manuellement constexpret la spécification d'exception pour correspondre au constructeur implicite.

L'utilisation = defaultapporte une certaine uniformité, car elle peut également être utilisée avec les constructeurs et destructeurs de copie / déplacement. Un constructeur de copie vide, par exemple, ne fera pas la même chose qu'un constructeur de copie par défaut (qui effectuera une copie par membre de ses membres). L'utilisation uniforme de la syntaxe = default(ou = delete) pour chacune de ces fonctions membres spéciales facilite la lecture de votre code en indiquant explicitement votre intention.

Joseph Mansfield
la source
Presque. 12.1 / 6: "Si ce constructeur par défaut écrit par l'utilisateur satisfait aux exigences d'un constexprconstructeur (7.1.5), le constructeur par défaut défini implicitement est constexpr."
Casey
En fait, 8.4.2 / 2 est plus informatif: "Si une fonction est explicitement mise par défaut sur sa première déclaration, (a) elle est implicitement considérée comme constexprsi la déclaration implicite serait, (b) elle est implicitement considérée comme ayant la même spécification d'exception comme si elle avait été implicitement déclarée (15.4), ... "Cela ne fait aucune différence dans ce cas précis, mais a en général foo() = default;un léger avantage par rapport à foo() {}.
Casey
2
Vous dites qu'il n'y a pas de différence, puis expliquez les différences?
@hvd Dans ce cas, il n'y a pas de différence, car la déclaration implicite ne le serait pas constexpr(puisqu'un membre de données n'est pas initialisé) et sa spécification d'exception autorise toutes les exceptions. Je vais clarifier cela.
Joseph Mansfield
2
Merci pour la clarification. Cependant, il semble toujours y avoir une différence avec constexpr(dont vous avez parlé ne devrait pas faire de différence ici): donne struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};seulement s1une erreur, non s2. Dans les deux clang et g ++.
10

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

#include <iostream>

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

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


int main() 
{ 
    int x = 5;
    new(&x)A(); // Call for empty constructor, which does nothing
    cout << x << endl;
    new(&x)B; // Call for default constructor
    cout << x << endl;
    new(&x)B(); // Call for default constructor + Value initialization
    cout << x << endl;
    return 0; 
} 

Production:

5
5
0

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

Slavenskij
la source
7
pourriez-vous expliquer cette syntaxe -> new (& x) A ();
Vencat le
5
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. Comme 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.
Slavenskij
Merci pour votre explication.
Vencat
1
J'obtiens
Adam.Er8
Même avec C ++ 14, j'obtiens
Mayank Bhushan
9

n2210 fournit quelques raisons:

La gestion des défauts présente plusieurs problèmes:

  • Les définitions de constructeur sont couplées; déclarer un constructeur supprime le constructeur par défaut.
  • La valeur par défaut du destructeur est inappropriée pour les classes polymorphes, nécessitant une définition explicite.
  • Une fois qu'une valeur par défaut est supprimée, il n'y a aucun moyen de la ressusciter.
  • Les implémentations par défaut sont souvent plus efficaces que les implémentations spécifiées manuellement.
  • Les implémentations non par défaut ne sont pas 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 une modification de la définition de la fonction membre car la valeur par défaut change avec la déclaration de membres supplémentaires.

Voir Rule-of-Three devient Rule-of-Five 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 déplacement opérateur d'affectation, et qu'une classe avec un destructeur explicitement déclaré et un constructeur de copie défini implicitement ou un opérateur d'affectation de copie défini implicitement est considérée comme obsolète

Communauté
la source
1
Ce sont des raisons d'avoir = defaulten général, plutôt que des raisons de faire = defaultsur un constructeur contre faire { }.
Joseph Mansfield
@JosephMansfield vrai, mais depuis {}était déjà une caractéristique de la langue avant l'introduction de =defaultces raisons ne repose implicitement sur la distinction (par exemple , « il n'y a pas moyen de ressusciter [défaut supprimé] » implique {}est pas équivalent à la valeur par défaut ).
Kyle Strand
7

C'est une question 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 faire en sorte que tout constructeur par défaut avec un corps vide soit considéré comme un candidat pour être un constructeur trivial, comme en utilisant =default. Après tout, les anciens constructeurs par défaut vides étaient du C ++ légal .

struct S { 
  int a; 
  S() {} // legal C++ 
};

Le fait que le compilateur comprenne ou non ce constructeur comme étant trivial n'est pas pertinent dans la plupart des cas en dehors des optimisations (manuelles ou du compilateur).

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

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

Dans le cas ci-dessus, le constructeur de copie écrit avec un corps vide est maintenant incorrect . Il ne copie plus rien. Il s'agit d'un ensemble de sémantiques très différent de la sémantique du constructeur de copie par défaut. Le comportement souhaité vous oblige à écrire du code:

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

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

Considérons alors l'opérateur d'affectation de copie qui peut devenir encore plus poilu, en particulier dans le cas non trivial. C'est une tonne de passe-partout que vous ne voulez pas avoir à écrire pour de nombreuses classes, mais vous y êtes obligé de toute façon en C ++ 03:

struct T { 
  std::shared_ptr<int> b; 
  T(); // the usual definitions
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // not actually needed for this simple example
      b = src.b; // non-trivial operation
    return *this;
};

C'est un cas simple, mais c'est déjà plus de code que vous ne voudriez jamais être obligé d'écrire pour un type aussi simple que T(surtout une fois que nous lançons des opérations de déplacement dans le mix). On ne peut pas se fier à un corps vide signifiant «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 désigner "remplir les valeurs par défaut", alors il n'y aurait aucun moyen de créer explicitement un constructeur de copie sans opération ou autre.

C'est encore 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 "ne rien faire" mais plutôt "faire toutes les choses que vous feriez normalement si elles ne sont pas supprimées". Par conséquent =default. Il est nécessaire pour surmonter les fonctions membres supprimées générées par le compilateur telles que les constructeurs de copie / déplacement et les opérateurs d'affectation. Il est alors juste "évident" de le faire fonctionner également pour le constructeur par défaut.

Cela aurait peut-être été bien de faire du constructeur par défaut avec des corps vides et des constructeurs de membre / base triviaux également considérés comme triviaux, tout comme ils l'auraient été avec =defaultsi seulement pour rendre le code plus ancien plus optimal dans certains cas, mais la plupart du code de bas niveau s'appuyant sur trivial Les constructeurs par défaut pour les optimisations reposent également sur des constructeurs de copie triviaux. Si vous allez devoir "réparer" tous vos anciens constructeurs de copie, ce n'est vraiment pas trop compliqué d'avoir à réparer tous vos anciens constructeurs par défaut non plus. Il est également beaucoup plus clair et évident d'utiliser un explicite =defaultpour désigner vos intentions.

Il y a quelques autres choses que les fonctions membres générées par le compilateur feront que vous devrez également apporter explicitement des modifications à la prise en charge. La prise en charge constexprdes constructeurs par défaut en est un exemple. C'est juste plus facile à utiliser mentalement =defaultque d'avoir à baliser des fonctions avec tous les autres mots-clés spéciaux et autres qui sont impliqués par =defaultet qui était l'un des thèmes de C ++ 11: rendre le langage plus facile. Il y a encore beaucoup de verrues et de compromis de compatibilité arrière, mais il est clair que c'est un grand pas en avant par rapport à C ++ 03 en ce qui concerne la facilité d'utilisation.

Sean Middleditch
la source
J'ai eu un problème où je pensais = defaultne faire a=0;et n'a pas été! J'ai dû l'abandonner en faveur de : a(0). Je ne sais toujours pas à quel point cela = defaultest utile , s'agit-il de performances? va-t-il casser quelque part si je ne l'utilise tout simplement pas = default? J'ai essayé de lire toutes les réponses ici acheter Je suis nouveau dans certains trucs c ++ et j'ai beaucoup de mal à le comprendre.
Aquarius Power
@AquariusPower: ce n'est pas "seulement" une question de performances mais également nécessaire dans certains cas autour d'exceptions et d'autres sémantiques. À savoir, 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 ou même interdire les types avec des opérations non triviales. Votre a=0exemple est dû au comportement des types triviaux, qui sont un sujet distinct (quoique lié).
Sean Middleditch
cela signifie-t-il qu'il est possible d'avoir = defaultet que la subvention le asera encore =0? en quelque sorte? pensez-vous que je pourrais créer une nouvelle question comme "comment avoir un constructeur = defaultet accorder les champs seront correctement initialisés?", btw j'ai eu le problème dans a structet pas a class, et l'application fonctionne correctement même sans utiliser = default, je peux ajoutez une structure minimale à cette question si elle est bonne :)
Aquarius Power
1
@AquariusPower: vous pouvez utiliser des initialiseurs de membres de données non statiques. Écrivez votre structure comme ceci: struct { int a = 0; };Si vous décidez ensuite que vous avez besoin d'un constructeur, vous pouvez le définir par défaut, mais notez que le type ne sera pas trivial (ce qui est bien).
Sean Middleditch
2

En raison de la dépréciation de std::is_podet de son alternative std::is_trivial && std::is_standard_layout, l'extrait de la réponse de @JosephMansfield devient:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() {}
};

int main() {
    static_assert(std::is_trivial_v<X>, "X should be trivial");
    static_assert(std::is_standard_layout_v<X>, "X should be standard layout");

    static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
    static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}

Notez que le Yest toujours de mise en page standard.

AnqurVanillapy
la source