Désordre d'initialisation par défaut, valeur et zéro

88

Je suis très confus au sujet de l'initialisation de la valeur et par défaut et zéro. et surtout lorsqu'ils se lancent dans les différents standards C ++ 03 et C ++ 11 (et C ++ 14 ).

Je cite et j'essaie d'étendre une très bonne réponse Value- / Default- / Zero- Init C ++ 98 et C ++ 03 ici pour la rendre plus générale car cela aiderait beaucoup d'utilisateurs si quelqu'un pouvait aider à remplir le besoin de lacunes pour avoir une bonne vue d'ensemble de ce qui se passe quand?

Un aperçu complet par des exemples en un mot:

Parfois, la mémoire retournée par l'opérateur new sera initialisée, et parfois cela ne le sera pas selon que le type que vous créez est un POD (données anciennes simples) , ou s'il s'agit d'une classe qui contient des membres POD et utilise un constructeur par défaut généré par le compilateur.

  • Dans C ++ 1998, il existe 2 types d'initialisation: zéro et initialisation par défaut
  • En C ++ 2003, un troisième type d'initialisation, l'initialisation de la valeur a été ajoutée.
  • Dans C ++ 2011 / C ++ 2014, seule l' initialisation de liste a été ajoutée et les règles d' initialisation de valeur / par défaut / zéro ont un peu changé.

Présumer:

struct A { int m; };                     
struct B { ~B(); int m; };               
struct C { C() : m(){}; ~C(); int m; };  
struct D { D(){}; int m; };             
struct E { E() = default; int m;}; /** only possible in c++11/14 */  
struct F {F(); int m;};  F::F() = default; /** only possible in c++11/14 */

Dans un compilateur C ++ 98, ce qui suit doit se produire :

  • new A - valeur indéterminée ( Aest POD)
  • new A()- initialisation à zéro
  • new B - construction par défaut ( B::mnon initialisée, Bnon-POD)
  • new B()- construction par défaut ( B::mnon initialisée)
  • new C - construction par défaut ( C::minitialisée à zéro, Cnon POD)
  • new C()- construction par défaut ( C::mest initialisé à zéro)
  • new D - construction par défaut ( D::mnon initialisée, Dnon-POD)
  • new D()- construction par défaut? ( D::mn'est pas initialisé)

Dans un compilateur conforme C ++ 03, les choses devraient fonctionner comme ceci:

  • new A - valeur indéterminée ( Aest POD)
  • new A() - value-initialize A, qui est une initialisation à zéro puisqu'il s'agit d'un POD.
  • new B - initialise par défaut (laisse B::mnon initialisé, Bn'est pas POD)
  • new B() - value-initializes Bqui initialise à zéro tous les champs puisque son ctor par défaut est généré par le compilateur et non défini par l'utilisateur.
  • new C - default-initializes C, qui appelle le ctor par défaut. ( C::mest initialisé à zéro, Cest non POD)
  • new C() - value-initializes C, qui appelle le ctor par défaut. ( C::mest initialisé à zéro)
  • new D - construction par défaut ( D::mnon initialisée, Dnon POD)
  • new D() - valeur-initialise D? , qui appelle le ctor par défaut ( D::mn'est pas initialisé)

Valeurs italiques et? sont des incertitudes, veuillez aider à corriger cela :-)

Dans un compilateur conforme C ++ 11, les choses devraient fonctionner comme ceci:

??? (aidez-moi si je commence ici, ça ira de toute façon mal)

Dans un compilateur conforme C ++ 14, les choses devraient fonctionner comme suit: ??? (Aidez-moi si je commence ici, ça ira de toute façon mal) (Brouillon basé sur la réponse)

  • new A - initialise par défaut A, compilateur gen. ctor, (laisse A::mnon initialisé) ( Aest POD)

  • new A() - value-initializes A, qui est une initialisation à zéro depuis 2. point dans [dcl.init] / 8

  • new B - initialise par défaut B, compilateur gen. ctor, (laisse B::mnon initialisé) ( Best non-POD)

  • new B() - value-initializes Bqui initialise à zéro tous les champs puisque son ctor par défaut est généré par le compilateur et non défini par l'utilisateur.

  • new C - default-initializes C, qui appelle le ctor par défaut. ( C::mest initialisé à zéro, Cest non POD)

  • new C() - value-initializes C, qui appelle le ctor par défaut. ( C::mest initialisé à zéro)

  • new D - initialise par défaut D( D::mn'est pas initialisé, Dn'est pas POD)

  • new D() - value-initializes D, qui appelle le ctor par défaut ( D::mn'est pas initialisé)

  • new E - default-initializes E, qui appelle le comp. gen. ctor. ( E::mn'est pas initialisé, E est non-POD)

  • new E() - value-initializes E, qui s'initialise à zéro Edepuis 2 points dans [dcl.init] / 8 )

  • new F - default-initializes F, qui appelle le comp. gen. ctor. ( F::mn'est pas initialisé, Fn'est pas POD)

  • new F() - value-initializes F, qui s'initialise par défaut F depuis 1. point dans [dcl.init] / 8 (la Ffonction ctor est fournie par l'utilisateur si elle est déclarée par l'utilisateur et non explicitement par défaut ou supprimée lors de sa première déclaration. Lien )

Gabriel
la source
il y a une bonne explication ici: en.cppreference.com/w/cpp/language/default_constructor
Richard Hodges
1
Pour autant que je sache, il n'y a qu'une différence entre C ++ 98 et C ++ 03 dans ces exemples. Le problème semble être décrit dans N1161 (il y a des révisions ultérieures de ce document) et CWG DR # 178 . Le libellé devait changer en C ++ 11 en raison de nouvelles fonctionnalités et d'une nouvelle spécification de POD, et il a de nouveau changé en C ++ 14 en raison de défauts dans le libellé C ++ 11, mais les effets dans ces cas ne sont pas modifiés .
dyp le
3
Bien que ennuyeux, cela struct D { D() {}; int m; };peut valoir la peine d'être inclus dans votre liste.
Yakk - Adam Nevraumont

Réponses:

24

C ++ 14 spécifie l'initialisation des objets créés avec newdans [expr.new] / 17 ([expr.new] / 15 dans C ++ 11, et la note n'était pas une note mais un texte normatif à l'époque):

Une nouvelle expression qui crée un objet de type Tinitialise cet objet comme suit:

  • Si le nouvel initialiseur est omis, l'objet est initialisé par défaut (8.5). [ Remarque: si aucune initialisation n'est effectuée, l'objet a une valeur indéterminée. - note de fin ]
  • Sinon, le nouvel initialiseur est interprété selon les règles d'initialisation de 8.5 pour l'initialisation directe .

L'initialisation par défaut est définie dans [dcl.init] / 7 (/ 6 en C ++ 11, et le libellé lui-même a le même effet):

Pour défaut initialiser un objet de type Tmoyen:

  • si Test un type de classe (éventuellement qualifié cv) (Article 9), le constructeur par défaut (12.1) pour Test appelé (et l'initialisation est mal formée si Tn'a pas de constructeur par défaut ou la résolution de surcharge (13.3) entraîne une ambiguïté ou une fonction supprimée ou inaccessible du contexte de l'initialisation);
  • si Test un type de tableau, chaque élément est initialisé par défaut ;
  • sinon, aucune initialisation n'est effectuée.

Donc

  • new Aprovoque uniquement l' Aappel du constructeur par défaut, qui ne s'initialise pas m. Valeur indéterminée. Devrait être la même chose pour new B.
  • new A() est interprété selon [dcl.init] / 11 (/ 10 en C ++ 11):

    Un objet dont l'initialiseur est un ensemble vide de parenthèses, c'est-à-dire (), doit être initialisé par valeur.

    Et maintenant, considérez [dcl.init] / 8 (/ 7 en C ++ 11 †):

    Pour la valeur initialiser un objet de type Tmoyen:

    • s'il Ts'agit d'un type de classe (éventuellement qualifié cv) (clause 9) sans constructeur par défaut (12.1) ou avec un constructeur par défaut fourni par l'utilisateur ou supprimé, alors l'objet est initialisé par défaut;
    • si Test un type de classe (éventuellement qualifié cv) sans constructeur par défaut fourni par l'utilisateur ou supprimé, alors l'objet est initialisé à zéro et les contraintes sémantiques pour l'initialisation par défaut sont vérifiées, et si T a un constructeur par défaut non trivial, l'objet est initialisé par défaut;
    • si Test un type de tableau, alors chaque élément est initialisé par valeur;
    • sinon, l'objet est initialisé à zéro.

    Par conséquent new A(), zéro-initialisation m. Et cela devrait être équivalent pour Aet B.

  • new Cet new C()réinitialisera l'objet par défaut, puisque la première puce du dernier devis s'applique (C a un constructeur par défaut fourni par l'utilisateur!). Mais, clairement, maintenant mest initialisé dans le constructeur dans les deux cas.


† Eh bien, ce paragraphe a un libellé légèrement différent en C ++ 11, ce qui ne modifie pas le résultat:

Pour la valeur initialiser un objet de type Tmoyen:

  • si Test un type de classe (éventuellement qualifié cv) (Clause 9) avec un constructeur fourni par l'utilisateur (12.1), alors le constructeur par défaut de T est appelé (et l'initialisation est mal formée si T n'a pas de constructeur par défaut accessible);
  • si Test un type de classe non-union (éventuellement qualifié cv) sans constructeur fourni par l'utilisateur, alors l'objet est initialisé à zéro et, si Tle constructeur par défaut implicitement déclaré est non trivial, ce constructeur est appelé.
  • si Test un type de tableau, alors chaque élément est initialisé par valeur;
  • sinon, l'objet est initialisé à zéro.
Columbo
la source
@Gabriel pas vraiment.
Columbo
ah donc vous parlez principalement de c ++ 14 et les références pour c ++ 11 sont données entre parenthèses
Gabriel
@Gabriel Correct. Je veux dire, C ++ 14 est la dernière norme, donc c'est au premier plan.
Columbo
1
La chose ennuyeuse à propos d'essayer de tracer les règles d'initialisation à travers les normes est que beaucoup de changements (la plupart? Tous?) Entre les normes publiées C ++ 14 et C ++ 11 se sont produits via des DR, tout comme de facto C ++ 11 . Et puis il y a aussi des DR post-C ++ 14 ...
TC
@Columbo Je ne comprends toujours pas pourquoi struct A { int m; }; struct C { C() : m(){}; int m; };produire des résultats différents et ce qui provoque l'initialisation de m dans A en premier lieu. J'ai ouvert un fil dédié à l'expérience que j'ai faite et j'apprécierai votre contribution pour clarifier le problème. Merci stackoverflow.com/questions/45290121/…
darkThoughts
12

La réponse suivante étend la réponse https://stackoverflow.com/a/620402/977038 qui servirait de référence pour C ++ 98 et C ++ 03

Citant la réponse

  1. Dans C ++ 1998, il existe 2 types d'initialisation: zéro et par défaut
  2. Dans C ++ 2003, un 3ème type d'initialisation, une initialisation de valeur a été ajoutée.

C ++ 11 (en référence à n3242)

Initialiseurs

8.5 Initializers [dcl.init] spécifie qu'une variable POD ou non POD peut être initialisée soit en tant que brace-or-equal-initializer qui peut être braced-init-list ou initializer-clause agrégée comme accolade-or-equal- initialiseur ou en utilisant (liste d'expressions) . Avant C ++ 11, seule la (liste d'expressions) ou la clause d'initialisation était prise en charge bien que la clause d'initialisation soit plus restreinte que ce que nous avons dans C ++ 11. Dans C ++ 11, la clause d'initialisation prend désormais en charge braced-init-list en dehors de l' expression d' affectationcomme c'était le cas en C ++ 03. La grammaire suivante résume la nouvelle clause prise en charge, où la partie est en gras est nouvellement ajoutée dans la norme C ++ 11.

initialiseur: initialiseur d'
    accolade ou d'égalité
    (liste d'expressions) initialiseur d'
accolade ou d'égalité:
    = clause d' initialisation
    liste d'
initialisation accolée clause d'initialisation:
    expression d'affectation
    liste d'
initialisation accolée liste d' initialisation:
    clause d'initialisation ... opt
    initializer-list, initializer-clause ... opt **
braced-init-list:
    {initializer-list, opt}
    {}

Initialisation

Comme C ++ 03, C ++ 11 prend toujours en charge trois formes d'initialisation


Remarque

La partie mise en évidence en gras a été ajoutée dans C ++ 11 et celle qui est barrée a été supprimée de C ++ 11.

  1. Type d'initialiseur: 8.5.5 [dcl.init] _zero-initialize_

Réalisé dans les cas suivants

  • Les objets avec une durée de stockage statique ou de thread sont initialisés à zéro
  • S'il y a moins d'initialiseurs que d'éléments de tableau, chaque élément non explicitement initialisé doit être initialisé à zéro
  • Lors de l'initialisation de la valeur , 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.

Initialiser à zéro un objet ou une référence de type T signifie:

  • si T est un type scalaire (3.9), l'objet est mis à la valeur 0 (zéro), prise comme une expression constante intégrale , convertie en T;
  • si T est un type de classe non union (éventuellement qualifié cv) , chaque membre de données non statique et chaque sous-objet de classe de base est initialisé à zéro et le remplissage est initialisé à zéro bit;
  • si T est un type d'union (éventuellement qualifié cv) , le premier membre de données nommé non statique de l'objet est initialisé à zéro et le remplissage est initialisé à zéro bit;
  • si T est un type tableau, chaque élément est initialisé à zéro;
  • si T est un type de référence, aucune initialisation n'est effectuée.

2. Type d'initialiseur: 8.5.6 [dcl.init] _default-initialize_

Réalisé dans les cas suivants

  • Si le nouvel initialiseur est omis, l'objet est initialisé par défaut; si aucune initialisation n'est effectuée, l'objet a une valeur indéterminée.
  • Si aucun initialiseur n'est spécifié pour un objet, l'objet est initialisé par défaut, sauf pour les objets avec une durée de stockage statique ou de thread
  • Lorsqu'une classe de base ou un membre de données non statique n'est pas mentionné dans une liste d'initialiseurs de constructeur et que ce constructeur est appelé.

Initialiser par défaut un objet de type T signifie:

  • si T est un type de classe non POD (éventuellement qualifié cv) (Article 9), 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 tableau, chaque élément est initialisé par défaut;
  • sinon, aucune initialisation n'est effectuée.

Remarque Jusqu'à C ++ 11, seuls les types de classe non POD avec une durée de stockage automatique étaient considérés comme étant initialisés par défaut lorsqu'aucun initialiseur n'est utilisé.


3. Type d'initialiseur: 8.5.7 [dcl.init] _value-initialize_

  1. Lorsqu'un objet (temporaire sans nom, variable nommée, durée de stockage dynamique ou membre de données non statique) dont l'initialiseur est un ensemble vide de parenthèses, c'est-à-dire () ou d'accolades {}

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é cv) sans constructeur fourni par l'utilisateur, alors chaque membre de données non statique et chaque composant de classe de base de T est initialisé par la valeur; 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 de tableau, alors chaque élément est initialisé par valeur;
  • sinon, l'objet est initialisé à zéro.

Donc pour résumer

Remarque La citation pertinente de la norme est mise en évidence en gras

  • nouveau A: initialise par défaut (laisse A :: m non initialisé)
  • new A (): zéro-initialiser A, car la valeur initialisée candidate n'a pas de constructeur par défaut fourni par l'utilisateur ou supprimé. 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é.
  • nouveau B: initialise par défaut (laisse B :: m non initialisé)
  • new B (): valeur-initialise B qui initialise à zéro tous les champs; 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é
  • new C: initialise par défaut C, qui appelle le ctor par défaut. si T est un type de classe (éventuellement qualifié cv) (clause 9), le constructeur par défaut pour T est appelé , De plus si le nouvel initialiseur est omis, l'objet est initialisé par défaut
  • new C (): valeur-initialise C, qui appelle le ctor par défaut. si T est un type de classe (éventuellement qualifié par cv) (clause 9) avec un constructeur fourni par l'utilisateur (12.1), alors le constructeur par défaut pour T est appelé. De plus, un objet dont l'initialiseur est un ensemble vide de parenthèses, c'est-à-dire (), doit être initialisé par la valeur
Abhijit
la source
0

Je peux confirmer qu'en C ++ 11, tout ce qui est mentionné dans la question sous C ++ 14 est correct, au moins selon les implémentations du compilateur.

Afin de vérifier cela, j'ai ajouté le code suivant à ma suite de tests . J'ai testé avec -std=c++11 -O3GCC 7.4.0, GCC 5.4.0, Clang 10.0.1 et VS 2017, et tous les tests ci-dessous réussissent.

#include <gtest/gtest.h>
#include <memory>

struct A { int m;                    };
struct B { int m;            ~B(){}; };
struct C { int m; C():m(){}; ~C(){}; };
struct D { int m; D(){};             };
struct E { int m; E() = default;     };
struct F { int m; F();               }; F::F() = default;

// We use this macro to fill stack memory with something else than 0.
// Subsequent calls to EXPECT_NE(a.m, 0) are undefined behavior in theory, but
// pass in practice, and help illustrate that `a.m` is indeed not initialized
// to zero. Note that we initially tried the more aggressive test
// EXPECT_EQ(a.m, 42), but it didn't pass on all compilers (a.m wasn't equal to
// 42, but was still equal to some garbage value, not zero).
//
#define FILL { int m = 42; EXPECT_EQ(m, 42); }

// We use this macro to fill heap memory with something else than 0, before
// doing a placement new at that same exact location. Subsequent calls to
// EXPECT_EQ(a->m, 42) are undefined behavior in theory, but pass in practice,
// and help illustrate that `a->m` is indeed not initialized to zero.
//
#define FILLH(b) std::unique_ptr<int> bp(new int(42)); int* b = bp.get(); EXPECT_EQ(*b, 42)

TEST(TestZero, StackDefaultInitialization)
{
    { FILL; A a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; B a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; C a; EXPECT_EQ(a.m, 0); }
    { FILL; D a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; F a; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackValueInitialization)
{
    { FILL; A a = A(); EXPECT_EQ(a.m, 0); }
    { FILL; B a = B(); EXPECT_EQ(a.m, 0); }
    { FILL; C a = C(); EXPECT_EQ(a.m, 0); }
    { FILL; D a = D(); EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a = E(); EXPECT_EQ(a.m, 0); }
    { FILL; F a = F(); EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackListInitialization)
{
    { FILL; A a{}; EXPECT_EQ(a.m, 0); }
    { FILL; B a{}; EXPECT_EQ(a.m, 0); }
    { FILL; C a{}; EXPECT_EQ(a.m, 0); }
    { FILL; D a{}; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a{}; EXPECT_EQ(a.m, 0); }
    { FILL; F a{}; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, HeapDefaultInitialization)
{
    { FILLH(b); A* a = new (b) A; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); B* a = new (b) B; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); C* a = new (b) C; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); F* a = new (b) F; EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapValueInitialization)
{
    { FILLH(b); A* a = new (b) A(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D(); EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F(); EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapListInitialization)
{
    { FILLH(b); A* a = new (b) A{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D{}; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F{}; EXPECT_EQ(a->m, 42); } // ~UB
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Les endroits où UB!est mentionné sont des comportements indéfinis, et le comportement réel dépendra probablement de nombreux facteurs ( a.mpeut être égal à 42, 0 ou une autre poubelle). Les endroits où ~UBest mentionné sont également des comportements indéfinis en théorie, mais en pratique, en raison de l'utilisation d'un placement nouveau, il est très peu probable que a->mce soit égal à autre chose que 42.

Boris Dalstein
la source