Initialisation zéro C ++ - Pourquoi «b» dans ce programme est-il non initialisé, mais «a» est initialisé?

135

Selon la réponse acceptée (et unique) pour cette question Stack Overflow ,

Définition du constructeur avec

MyTest() = default;

va à la place initialiser l'objet à zéro.

Alors pourquoi ce qui suit,

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{};
    bar b{};
    std::cout << a.a << ' ' << b.b;
}

produire cette sortie:

0 32766

Les deux constructeurs définis sont par défaut? Droite? Et pour les types POD, l'initialisation par défaut est zéro-initialisation.

Et selon la réponse acceptée pour cette question ,

  1. Si un membre POD n'est pas initialisé dans le constructeur ni via l'initialisation en classe C ++ 11, il est initialisé par défaut.

  2. La réponse est la même quelle que soit la pile ou le tas.

  3. Dans C ++ 98 (et pas après), new int () a été spécifié comme effectuant une initialisation zéro.

Bien que j'aie essayé d'envelopper ma tête (quoique minuscule ) autour des constructeurs par défaut et de l' initialisation par défaut , je n'ai pas pu trouver d'explication.

Canard Dodgers
la source
3
Fait intéressant, j'obtiens même un avertissement pour b: main.cpp: 18: 34: avertissement: 'b.bar::b' est utilisé non initialisé dans cette fonction [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl
8
barLe constructeur de est fourni par l'utilisateur alors que foole constructeur de est celui par défaut.
Jarod42
2
@PeteBecker, je comprends cela. Comment pourrais-je en quelque sorte secouer un peu ma RAM pour que s'il n'y en avait pas, ce devrait être autre chose. ;) ps J'ai exécuté le programme une dizaine de fois. Ce n'est pas un gros programme. Vous pouvez l'exécuter et le tester sur votre système. aest zéro. bn'est pas. Semble aest initialisé.
Duck Dodgers
2
@JoeyMallone Concernant "comment est-il fourni par l'utilisateur": Il n'y a aucune garantie que la définition de bar::bar()soit visible dans main()- il pourrait être défini dans une unité de compilation séparée et faire quelque chose de très non trivial alors que main()seule la déclaration est visible. Je pense que vous conviendrez que ce comportement ne devrait pas changer selon que vous placez bar::bar()la définition de dans une unité de compilation distincte ou non (même si la situation dans son ensemble n'est pas intuitive).
Max Langhof
2
@balki Ou int a = 0;voulez-vous être vraiment explicite.
NathanOliver

Réponses:

109

Le problème ici est assez subtil. Vous penseriez que

bar::bar() = default;

vous donnerait un constructeur par défaut généré par le compilateur, et c'est le cas, mais il est maintenant considéré comme fourni par l'utilisateur. [dcl.fct.def.default] / 5 déclare:

Les fonctions explicitement par défaut et les fonctions implicitement déclarées sont collectivement appelées fonctions par défaut, et l'implémentation doit leur fournir des définitions implicites ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign ]), ce qui pourrait signifier les définir comme supprimés. Une fonction est fournie par l'utilisateur si elle est déclarée par l'utilisateur et n'est pas explicitement définie par défaut ou supprimée lors de sa première déclaration.Une fonction explicitement par défaut fournie par l'utilisateur (c'est-à-dire explicitement par défaut après sa première déclaration) est définie au point où elle est explicitement définie par défaut; si une telle fonction est implicitement définie comme supprimée, le programme est mal formé. [Remarque: déclarer une fonction comme étant par défaut après sa première déclaration peut fournir une exécution efficace et une définition concise tout en permettant une interface binaire stable à une base de code en évolution. - note de fin]

accent le mien

Nous pouvons donc voir que, puisque vous n'avez pas défini la valeur par défaut bar()lors de la première déclaration, il est désormais considéré comme fourni par l'utilisateur. A cause de cela [dcl.init] /8.2

si T est 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;

ne s'applique plus et nous ne valorisons pas l'initialisation bmais l'initialisation par défaut par [dcl.init] /8.1

si T est un type de classe (éventuellement qualifié cv) ([class]) sans constructeur par défaut ([class.default.ctor]) ou un constructeur par défaut fourni ou supprimé par l'utilisateur, alors l'objet est initialisé par défaut ;

NathanOliver
la source
52
Je veux dire (*_*)... Si même pour utiliser les constructions de base de la langue, j'ai besoin de lire les petits caractères du brouillon de langue, alors Alléluia! Mais cela semble probablement être ce que vous dites.
Duck Dodgers
12
@balki Oui, faire bar::bar() = defaulthors ligne équivaut à faire en bar::bar(){}ligne.
NathanOliver
15
@JoeyMallone Ouais, C ++ peut être assez compliqué. Je ne sais pas quelle en est la raison.
NathanOliver
3
S'il y a une déclaration précédente, alors une définition ultérieure avec le mot-clé par défaut ne mettra PAS à zéro l'initialisation des membres. Droite? C'est correct. C'est ce qui se passe ici.
NathanOliver
6
La raison est là dans votre devis: le but d'un défaut hors ligne est de "fournir une exécution efficace et une définition concise tout en permettant une interface binaire stable à une base de code en évolution", en d'autres termes, vous permettre de passer à un corps écrit par l'utilisateur plus tard si nécessaire sans casser ABI. Notez que la définition hors ligne n'est pas implicitement en ligne et ne peut donc apparaître que dans une TU par défaut; un autre TU voyant la définition de classe seule n'a aucun moyen de savoir si elle est explicitement définie par défaut.
TC
25

La différence de comportement vient du fait que, selon [dcl.fct.def.default]/5, bar::barest fourni par l'utilisateurfoo::foon'est pas 1 . En conséquence, foo::foosera la valeur-initialiser ses membres ( ce qui signifie: zéro initialize foo::a ) mais bar::barrestera non initialisé 2 .


1) [dcl.fct.def.default]/5

Une fonction est fournie par l'utilisateur si elle est déclarée par l'utilisateur et n'est pas explicitement définie par défaut ou supprimée lors de sa première déclaration.

2)

Depuis [dcl.init # 6] :

Initialiser par valeur un objet de type T signifie:

  • si T est un type de classe (éventuellement qualifié cv) sans constructeur par défaut ([class.ctor]) ou un constructeur par défaut fourni par l'utilisateur ou supprimé, alors l'objet est initialisé par défaut;

  • si T est 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;

  • ...

Depuis [dcl.init.list] :

L'initialisation en liste d'un objet ou d'une référence de type T est définie comme suit:

  • ...

  • Sinon, si la liste d'initialiseurs ne contient aucun élément et que T est un type de classe avec un constructeur par défaut, l'objet est initialisé par valeur.

D'après la réponse de Vittorio Romeo

YSC
la source
10

De cppreference :

L'initialisation des agrégats initialise les agrégats. C'est une forme d'initialisation de liste.

Un agrégat est l'un des types suivants:

[couper]

  • type de classe [snip], qui a

    • [snip] (il existe des variantes pour différentes versions standard)

    • aucun constructeur fourni par l'utilisateur, hérité ou explicite (les constructeurs explicitement par défaut ou supprimés sont autorisés)

    • [snip] (il y a plus de règles, qui s'appliquent aux deux classes)

Compte tenu de cette définition, fooest un agrégat, tandis que barne l'est pas (il a un constructeur non défini par défaut fourni par l'utilisateur).

Par conséquent, pour foo, T object {arg1, arg2, ...};est la syntaxe pour l'initialisation agrégée.

Les effets de l'initialisation des agrégats sont:

  • [snip] (certains détails sans rapport avec ce cas)

  • Si le nombre de clauses d'initialisation est inférieur au nombre de membres ou si la liste d'initialisation est complètement vide, les membres restants sont initialisés par valeur .

La a.avaleur est donc initialisée, ce qui intsignifie une initialisation nulle.

Car bar, T object {};d'autre part, il y a l'initialisation de la valeur (de l'instance de classe, pas l'initialisation de la valeur des membres!). Puisqu'il s'agit d'un type de classe avec un constructeur par défaut, le constructeur par défaut est appelé. Le constructeur par défaut que vous avez défini par défaut initialise les membres (en raison de l'absence d'initialiseurs de membres), qui en cas de int(avec stockage non statique) laisse b.bavec une valeur indéterminée.

Et pour les types de pod, l'initialisation par défaut est l'initialisation zéro.

Non, c'est faux.


PS Un mot sur votre expérience et votre conclusion: voir que la sortie est égale à zéro ne signifie pas nécessairement que la variable a été initialisée à zéro. Zéro est un nombre parfaitement possible pour une valeur de garbage.

pour cela, j'ai exécuté le programme peut-être 5 à 6 fois avant de publier et environ 10 fois maintenant, a est toujours zéro. b change un peu.

Le fait que la valeur ait été la même plusieurs fois ne signifie pas nécessairement qu'elle a été initialisée non plus.

J'ai aussi essayé avec set (CMAKE_CXX_STANDARD 14). Le résultat était le même.

Le fait que le résultat soit le même avec plusieurs options du compilateur ne signifie pas que la variable est initialisée. (Bien que dans certains cas, le changement de version standard peut changer si elle est initialisée).

Comment pourrais-je en quelque sorte secouer un peu ma RAM pour que s'il y avait zéro là-bas, ce devrait maintenant être autre chose

Il n'existe aucun moyen garanti en C ++ de faire apparaître une valeur de valeur non initialisée différente de zéro.

Le seul moyen de savoir qu'une variable est initialisée est de comparer le programme aux règles du langage et de vérifier que les règles disent qu'il est initialisé. Dans ce cas a.aest bien initialisé.

Eerorika
la source
"Le constructeur par défaut que vous avez défini par défaut initialise les membres (en raison de l'absence d'initialiseurs de membre), ce qui, dans le cas de int, lui laisse une valeur indéterminée." -> hein! "pour les types de pod, l'initialisation par défaut est zéro-initialisation." ou ai-je tort?
Duck Dodgers
2
@JoeyMallone L'initialisation par défaut des types de POD n'est pas une initialisation.
NathanOliver
@NathanOliver, alors je suis encore plus confus. Alors comment se fait-il aest initialisé. Je pensais que l' ainitialisation par défaut est initialisée par défaut et l'initialisation par défaut d'un POD membre est une initialisation zéro. Est adonc heureusement toujours à zéro, peu importe combien de fois je lance ce programme.
Duck Dodgers du
@JoeyMallone Then how come a is initialized.Parce qu'il s'agit d'une valeur initialisée. I was thinking a is default initializedCe n'est pas.
eerorika
3
@JoeyMallone Ne vous en faites pas. Vous pouvez créer un livre à partir de l'initialisation en C ++. Si vous avez une chance, CppCon sur youtube a quelques vidéos sur l'initialisation, la plus décevante (comme en soulignant à quel point c'est mauvais) étant youtube.com/watch?v=7DTlWPgX6zs
NathanOliver
0

Meh, j'ai essayé d'exécuter l'extrait que vous avez fourni en tant que test.cpp, via gcc & clang et plusieurs niveaux d'optimisation:

steve@steve-pc /tmp> g++ -o test.gcc.O0 test.cpp
                                                                              [ 0s828 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.O2 -O2 test.cpp
                                                                              [ 0s901 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.Os -Os test.cpp
                                                                              [ 0s875 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O0
0 32764                                                                       [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O2
0 0                                                                           [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.Os
0 0                                                                           [ 0s003 | Jan 27 01:16PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
                                                                              [ 1s089 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.Os -Os test.cpp
                                                                              [ 1s058 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O2 -O2 test.cpp
                                                                              [ 1s109 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 274247888                                                                   [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.Os
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O2
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 2127532240                                                                  [ 0s002 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 344211664                                                                   [ 0s004 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 1694408912                                                                  [ 0s004 | Jan 27 01:18PM ]

C'est donc là que ça devient intéressant, cela montre clairement que la construction clang O0 lit des nombres aléatoires, vraisemblablement de l'espace de pile.

J'ai rapidement consulté mon IDA pour voir ce qui se passait:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  int result; // eax
  unsigned int v6; // [rsp+8h] [rbp-18h]
  unsigned int v7; // [rsp+10h] [rbp-10h]
  unsigned __int64 v8; // [rsp+18h] [rbp-8h]

  v8 = __readfsqword(0x28u); // alloca of 0x28
  v7 = 0; // this is foo a{}
  bar::bar((bar *)&v6); // this is bar b{}
  v3 = std::ostream::operator<<(&std::cout, v7); // this is clearly 0
  v4 = std::operator<<<std::char_traits<char>>(v3, 32LL); // 32 = 0x20 = ' '
  result = std::ostream::operator<<(v4, v6); // joined as cout << a.a << ' ' << b.b, so this is reading random values!!
  if ( __readfsqword(0x28u) == v8 ) // stack align check
    result = 0;
  return result;
}

Maintenant, qu'est-ce que ça bar::bar(bar *this)fait?

void __fastcall bar::bar(bar *this)
{
  ;
}

Hmm, rien. Nous avons dû recourir à l'assemblage:

.text:00000000000011D0                               ; __int64 __fastcall bar::bar(bar *__hidden this)
.text:00000000000011D0                                               public _ZN3barC2Ev
.text:00000000000011D0                               _ZN3barC2Ev     proc near               ; CODE XREF: main+20p
.text:00000000000011D0
.text:00000000000011D0                               var_8           = qword ptr -8
.text:00000000000011D0
.text:00000000000011D0                               ; __unwind {
.text:00000000000011D0 55                                            push    rbp
.text:00000000000011D1 48 89 E5                                      mov     rbp, rsp
.text:00000000000011D4 48 89 7D F8                                   mov     [rbp+var_8], rdi
.text:00000000000011D8 5D                                            pop     rbp
.text:00000000000011D9 C3                                            retn
.text:00000000000011D9                               ; } // starts at 11D0
.text:00000000000011D9                               _ZN3barC2Ev     endp

Donc oui, c'est juste, rien, ce que le constructeur fait essentiellement this = this. Mais nous savons qu'il charge en fait des adresses de pile non initialisées aléatoires et les imprime.

Et si nous fournissons explicitement des valeurs pour les deux structures?

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{0};
    bar b{0};
    std::cout << a.a << ' ' << b.b;
}

Hit up clang, oopsie:

steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
test.cpp:17:9: error: no matching constructor for initialization of 'bar'
    bar b{0};
        ^~~~
test.cpp:8:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion
      from 'int' to 'const bar' for 1st argument
struct bar {
       ^
test.cpp:8:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion
      from 'int' to 'bar' for 1st argument
struct bar {
       ^
test.cpp:13:6: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
bar::bar() = default;
     ^
1 error generated.
                                                                              [ 0s930 | Jan 27 01:35PM ]

Destin similaire avec g ++ également:

steve@steve-pc /tmp> g++ test.cpp
test.cpp: In function int main()’:
test.cpp:17:12: error: no matching function for call to bar::bar(<brace-enclosed initializer list>)’
     bar b{0};
            ^
test.cpp:8:8: note: candidate: bar::bar()’
 struct bar {
        ^~~
test.cpp:8:8: note:   candidate expects 0 arguments, 1 provided
test.cpp:8:8: note: candidate: constexpr bar::bar(const bar&)’
test.cpp:8:8: note:   no known conversion for argument 1 from int to const bar&’
test.cpp:8:8: note: candidate: constexpr bar::bar(bar&&)’
test.cpp:8:8: note:   no known conversion for argument 1 from int to bar&&’
                                                                              [ 0s718 | Jan 27 01:35PM ]

Cela signifie donc que c'est effectivement une initialisation directe bar b(0), pas une initialisation agrégée.

C'est probablement parce que si vous ne fournissez pas d'implémentation de constructeur explicite, cela pourrait potentiellement être un symbole externe, par exemple:

bar::bar() {
  this.b = 1337; // whoa
}

Le compilateur n'est pas assez intelligent pour déduire cela comme un appel sans opération / en ligne dans une étape non optimisée.

Steve Fan
la source