Selon la réponse acceptée (et uniquement) pour cette question de débordement de pile ,
Définition du constructeur avec
MyTest() = default;
va à la place initialiser à zéro l'objet.
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 l'initialisation zéro.
Et selon la réponse acceptée pour cette question ,
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.
La réponse est la même indépendamment de la pile ou du tas.
En C++ 98 (et non après), new int () a été spécifié comme effectuant une initialisation nulle.
En dépit d'essayer d'envelopper ma (quoique petite tête ) autour constructeurs par défaut et initialisation par défaut , je n'a pas pu trouver une explication.
Le problème ici est assez subtil. On pourrait penser que
bar::bar() = default;
vous donnerait un constructeur par défaut généré par le compilateur, et il le fait, mais il est maintenant considéré comme fourni par l'utilisateur. [dcl.fct.def.default]/5 indique:
Les fonctions par défaut explicites et les fonctions déclarées implicitement sont appelées collectivement 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 non explicitement par défaut ou supprimée lors de sa première déclaration. Une fonction par défaut explicitement fournie par l'utilisateur ( c'est-à-dire explicitement par défaut après sa première déclaration) est défini au point où il est explicitement par défaut; si une telle fonction est implicitement définie comme supprimée, le programme est mal formé. [Remarque: Déclarer une fonction 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]
mettre l'accent
Nous pouvons donc voir que puisque vous n'avez pas utilisé par défaut bar()
lorsque vous l'avez déclaré pour la première fois, il est maintenant considéré comme fourni par l'utilisateur. À cause de cela [dcl.init] /8.2
si T est un type de classe (éventuellement qualifié par 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 b
mais plutôt l'initialisation par défaut par [dcl.init] /8.1
si T est un type de classe (éventuellement qualifié par cv) ([class]) sans constructeur par défaut ([class.default.ctor]) ou un constructeur par défaut fourni par l'utilisateur ou supprimé, alors l'objet est initialisé par défaut ;
La différence de comportement vient du fait que, selon [dcl.fct.def.default]/5
, bar::bar
est fourni par l'utilisateur où foo::foo
n'est pas1. En conséquence, foo::foo
will value-initialize ses membres (ce qui signifie: zero-initializefoo::a
) mais bar::bar
restera 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)
De [dcl.init # 6] :
Pour initialiser en valeur un objet de type T, cela signifie:
si T est un type de classe (éventuellement qualifié par cv) sans constructeur par défaut ([class.ctor]) ou un constructeur par défaut fourni ou supprimé par l'utilisateur, 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;
...
De [dcl.init.list] :
L'initialisation de liste d'un objet ou d'une référence de type T est définie comme suit:
...
Sinon, si la liste d'initialisation ne contient aucun élément et T est un type de classe avec un constructeur par défaut, l'objet est initialisé en valeur.
De cppreference :
L'initialisation d'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, foo
est un agrégat, tandis que bar
ne l'est pas (il a un constructeur non fourni 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 agrégée sont les suivants:
[snip] (certains détails non pertinents dans ce cas)
Si le nombre de clauses d'initialisation est inférieur au nombre de membres ou que la liste d'initialisation est complètement vide, les membres restants sont initialisés en valeur .
Donc a.a
est une valeur initialisée, ce qui pour int
signifie une initialisation nulle.
Pour bar
, T object {};
d'autre part est 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), ce qui en cas de int
(avec stockage non statique) laisse b.b
avec une valeur indéterminée.
Et pour les pod-types, l'initialisation par défaut est zéro-initialisation.
Non, c'est faux.
P.S. Un mot sur votre expérience et votre conclusion: Voir que la sortie est nulle 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 déchets.
pour cela, j'ai exécuté le programme peut-être 5 à 6 fois avant de poster et environ 10 fois maintenant, a est toujours nul. 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 également 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 de compilation ne signifie pas que la variable est initialisée. (Bien que dans certains cas, la modification de la version standard puisse changer si elle est initialisée).
Comment pourrais-je en quelque sorte secouer mon RAM un peu pour que s'il y avait zéro là, ça devrait maintenant être autre chose
Il n'y a aucun moyen garanti en C++ de faire apparaître une valeur non initialisée non nulle.
La seule façon 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'elle est initialisée. Dans ce cas a.a
est en effet initialisé.
Meh, j'ai essayé d'exécuter l'extrait de code 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 cela devient intéressant, cela montre clairement que la construction de clang O0 lit des nombres aléatoires, vraisemblablement l'empilement d'espace.
J'ai rapidement tourné mon IDA pour voir ce qui se passe:
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, que fait bar::bar(bar *this)
?
void __fastcall bar::bar(bar *this)
{
;
}
Hmm, rien. Nous avons dû recourir à Assembly:
.text:00000000000011D0 ; __int64 __fastcall bar::bar(bar *__hidden this)
.text:00000000000011D0 public _ZN3barC2Ev
.text:00000000000011D0 _ZN3barC2Ev proc near ; CODE XREF: main+20↓p
.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
Alors oui, c'est juste, rien, ce que le constructeur fait essentiellement est this = this
. Mais nous savons qu'il charge réellement 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 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 qu'il s'agit effectivement d'une initialisation directe bar b(0)
, et non d'une initialisation agrégée.
Cela est probablement dû au fait 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.