En quoi «= default» est-il différent de «{}» pour le constructeur et le destructeur par défaut?

169

J'ai initialement posté cela comme une question uniquement sur les destructeurs, mais maintenant j'ajoute la considération du constructeur par défaut. Voici la question originale:

Si je veux donner à ma classe un destructeur qui est virtuel, mais qui est sinon le même que ce que le compilateur générerait, je peux utiliser =default:

class Widget {
public:
   virtual ~Widget() = default;
};

Mais il semble que je puisse obtenir le même effet avec moins de frappe en utilisant une définition vide:

class Widget {
public:
   virtual ~Widget() {}
};

Y a-t-il une manière dont ces deux définitions se comportent différemment?

D'après les réponses publiées pour cette question, la situation du constructeur par défaut semble similaire. Étant donné qu'il n'y a presque aucune différence de signification entre " =default" et " {}" pour les destructeurs, n'y a-t-il pas de différence de signification similaire entre ces options pour les constructeurs par défaut? Autrement dit, en supposant que je veuille créer un type où les objets de ce type seront à la fois créés et détruits, pourquoi voudrais-je dire

Widget() = default;

au lieu de

Widget() {}

?

Je m'excuse si l'extension de cette question après sa publication d'origine enfreint certaines règles SO. Publier une question presque identique pour les constructeurs par défaut m'a semblé être l'option la moins souhaitable.

KnowItAllWannabe
la source
1
Pas que je sache, mais = defaultest imo plus explicite, et est cohérent avec le support pour cela avec les constructeurs.
chris
11
Je ne sais pas avec certitude, mais je pense que le premier est conforme à la définition de «destructeur trivial», tandis que le second ne le fait pas. Il en std::has_trivial_destructor<Widget>::valueva de même truepour le premier, mais falsepour le second. Quelles sont les implications de cela, je ne sais pas non plus. :)
GManNickG
10
Un destructeur virtuel n'est jamais anodin.
Luc Danton
@LucDanton: Je suppose qu'ouvrir les yeux et regarder le code fonctionnerait aussi! Merci d'avoir corrigé.
GManNickG
Connexes: stackoverflow.com/questions/20828907/…
Gabriel Staples

Réponses:

103

C'est une question complètement différente lorsqu'on pose des questions sur les constructeurs et sur les destructeurs.

Si votre destructeur l'est virtual, alors la différence est négligeable, comme l'a souligné Howard . Cependant, si votre destructeur n'était pas virtuel , c'est une histoire complètement différente. Il en va de même pour les constructeurs.

Utiliser la = defaultsyntaxe pour les fonctions membres spéciales (constructeur par défaut, copier / déplacer les constructeurs / affectation, destructeurs, etc.) signifie quelque chose de très différent de simplement faire {}. Avec ce dernier, la fonction devient "fournie par l'utilisateur". Et cela change tout.

C'est une classe triviale selon la définition de C ++ 11:

struct Trivial
{
  int foo;
};

Si vous essayez d'en construire un par défaut, le compilateur générera automatiquement un constructeur par défaut. Il en va de même pour la copie / mouvement et la destruction. Étant donné que l'utilisateur n'a fourni aucune de ces fonctions membres, la spécification C ++ 11 considère qu'il s'agit d'une classe «triviale». Il est donc légal de faire cela, comme mémoriser leur contenu pour les initialiser et ainsi de suite.

Ce:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Comme son nom l'indique, ce n'est plus anodin. Il a un constructeur par défaut fourni par l'utilisateur. Peu importe qu'il soit vide; en ce qui concerne les règles de C ++ 11, cela ne peut pas être un type trivial.

Ce:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Encore une fois, comme son nom l'indique, il s'agit d'un type trivial. Pourquoi? Parce que vous avez dit au compilateur de générer automatiquement le constructeur par défaut. Le constructeur n'est donc pas "fourni par l'utilisateur". Et par conséquent, le type compte comme trivial, car il n'a pas de constructeur par défaut fourni par l'utilisateur.

La = defaultsyntaxe est principalement là pour faire des choses comme la copie de constructeurs / affectation, lorsque vous ajoutez des fonctions membres qui empêchent la création de telles fonctions. Mais cela déclenche également un comportement spécial du compilateur, donc il est également utile dans les constructeurs / destructeurs par défaut.

Nicol Bolas
la source
2
Le problème clé semble donc être de savoir si la classe résultante est triviale, et sous-jacent ce problème est la différence entre une fonction spéciale déclarée par l'utilisateur (ce qui est le cas pour les =defaultfonctions) et des fonctions fournies par l'utilisateur (ce qui est le cas pour {}). Les fonctions déclarées par l'utilisateur et fournies par l'utilisateur peuvent empêcher la génération d'autres fonctions membres spéciales (par exemple, un destructeur déclaré par l'utilisateur empêche la génération des opérations de déplacement), mais seule une fonction spéciale fournie par l'utilisateur rend une classe non triviale. Droite?
KnowItAllWannabe
@KnowItAllWannabe: C'est l'idée générale, oui.
Nicol Bolas
Je choisis ceci comme réponse acceptée, uniquement parce qu'elle couvre à la fois les constructeurs et (en référence à la réponse de Howard) les destructeurs.
KnowItAllWannabe
Semble être un mot manquant ici "en ce qui concerne les règles de C ++ 11, vous avez les droits d'un type trivial" Je le réparerais mais je ne suis pas tout à fait sûr à 100% de ce qui était prévu.
jcoder
2
= defaultsemble être utile pour forcer le compilateur à générer un constructeur par défaut malgré la présence d'autres constructeurs; le constructeur par défaut n'est pas déclaré implicitement si d'autres constructeurs déclarés par l'utilisateur sont fournis.
bgfvdu3w
42

Ils sont tous les deux non triviaux.

Ils ont tous deux la même spécification noexcept en fonction de la spécification noexcept des bases et des membres.

La seule différence que je détecte jusqu'à présent est que si Widgetcontient une base ou un membre avec un destructeur inaccessible ou supprimé:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

Ensuite, la =defaultsolution se compilera, mais Widgetne sera pas de type destructible. C'est-à-dire que si vous essayez de détruire un Widget, vous obtiendrez une erreur de compilation. Mais si vous ne le faites pas, vous avez un programme qui fonctionne.

Otoh, si vous fournissez le destructeur fourni par l' utilisateur , les choses ne se compileront pas, que vous détruisiez ou non un Widget:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.
Howard Hinnant
la source
9
Intéressant: en d'autres termes, avec =default;le compilateur ne générera pas le destructeur à moins qu'il ne soit utilisé, et ne déclenchera donc pas d'erreur. Cela me semble bizarre, même si ce n'est pas forcément un bug. Je ne peux pas imaginer que ce comportement soit obligatoire dans la norme.
Nik Bougalis
"Ensuite, la solution = default se compilera" Non, ce ne sera pas le cas. Juste testé en vs
nano
Quel était le message d'erreur et quelle version de VS?
Howard Hinnant le
35

La différence importante entre

class B {
    public:
    B(){}
    int i;
    int j;
};

et

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

est que le constructeur par défaut défini avec B() = default;est considéré comme non défini par l'utilisateur . Cela signifie qu'en cas d' initialisation de valeur comme dans

B* pb = new B();  // use of () triggers value-initialization

un type spécial d'initialisation qui n'utilise pas du tout de constructeur aura lieu et pour les types intégrés, cela entraînera une initialisation à zéro . Dans le cas où B(){}cela n'aura pas lieu. La norme C ++ n3337 § 8.5 / 7 dit

Initialiser par valeur un objet de type T signifie:

- si T est un type de classe (éventuellement qualifié cv) (Clause 9) avec un constructeur fourni par l'utilisateur (12.1), alors le constructeur par défaut pour T est appelé (et l'initialisation est mal formée si T n'a pas de constructeur par défaut accessible );

- si T est un type de classe non-union (éventuellement qualifié par cv) sans constructeur fourni par l'utilisateur , alors l'objet est initialisé à zéro et, si le constructeur implicitement déclaré par défaut de T n'est pas trivial, ce constructeur est appelé.

- si T est un type tableau, alors chaque élément est initialisé par une valeur; - sinon, l'objet est initialisé à zéro.

Par exemple:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

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

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

résultat possible:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd

4pie0
la source
Alors, pourquoi "{}" et "= default" initialisent-ils toujours un std :: string ideone.com/LMv5Uf ?
nawfel bgh
1
@nawfelbgh Le constructeur par défaut A () {} appelle le constructeur par défaut pour std :: string car il s'agit d'un type non POD. Le cteur par défaut de std :: string l' initialise à une chaîne vide de taille 0. Le cteur par défaut pour les scalaires ne fait rien: les objets à durée de stockage automatique (et leurs sous-objets) sont initialisés à des valeurs indéterminées.
4pie0