C ++ Null-Initialisierung - Warum wird "b" in diesem Programm nicht initialisiert, aber "a" wird initialisiert?

135

Nach der angenommen (und einzigen) Antwort für diese Frage Stack - Überlaufes ,

Konstruktor definieren mit

MyTest() = default;

initialisiert stattdessen das Objekt auf Null.

Warum macht dann das Folgende:

#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;
}

Produziere diese Ausgabe:

0 32766

Beide definierten Konstruktoren sind Standard? Richtig? Bei POD-Typen ist die Standardinitialisierung die Nullinitialisierung.

Und gemäß der akzeptierten Antwort auf diese Frage ,

  1. Wenn ein POD-Mitglied weder im Konstruktor noch über die C ++ 11-Initialisierung in der Klasse initialisiert wird, wird es standardmäßig initialisiert.

  2. Die Antwort ist unabhängig von Stapel oder Haufen dieselbe.

  3. In C ++ 98 (und nicht danach) wurde new int () als Nullinitialisierung angegeben.

Obwohl ich versucht habe, meinen (wenn auch winzigen ) Kopf um Standardkonstruktoren und Standardinitialisierung zu wickeln , konnte ich keine Erklärung finden.

Duck Dodgers
quelle
3
Interessanterweise erhalte ich sogar eine Warnung für b: main.cpp: 18: 34: warning: 'b.bar::b' wird in dieser Funktion nicht initialisiert verwendet. [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
Tkausl
8
barDer Konstruktor wird vom Benutzer bereitgestellt, während fooder Konstruktor der Standard ist.
Jarod42
2
@ PeteBecker, das verstehe ich. Wie könnte ich meinen RAM ein wenig schütteln, so dass es jetzt etwas anderes sein sollte, wenn es dort Null gab. ;) ps Ich habe das Programm ein Dutzend Mal ausgeführt. Es ist kein großes Programm. Sie können es ausführen und auf Ihrem System testen. aist Null. bist nicht. Scheint ainitialisiert zu sein.
Duck Dodgers
2
@JoeyMallone In Bezug auf "Wie wird es vom Benutzer bereitgestellt?": Es gibt keine Garantie dafür, dass die Definition von bar::bar()sichtbar ist in main()- es kann in einer separaten Kompilierungseinheit definiert sein und etwas sehr nicht Triviales tun, während main()nur in der Deklaration sichtbar ist. Ich denke, Sie werden zustimmen, dass sich dieses Verhalten nicht ändern sollte, je nachdem, ob Sie bar::bar()die Definition in einer separaten Kompilierungseinheit platzieren oder nicht (auch wenn die gesamte Situation nicht intuitiv ist).
Max Langhof
2
@balki Oder int a = 0;willst du wirklich explizit sein ?
NathanOliver

Antworten:

109

Das Problem hier ist ziemlich subtil. Das würdest du denken

bar::bar() = default;

würde Ihnen einen vom Compiler generierten Standardkonstruktor geben, und das tut er, aber er wird jetzt als vom Benutzer bereitgestellt betrachtet. [dcl.fct.def.default] / 5 besagt:

Explizit voreingestellte Funktionen und implizit deklarierte Funktionen werden gemeinsam als voreingestellte Funktionen bezeichnet, und die Implementierung muss implizite Definitionen für sie bereitstellen ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign] ]), was bedeuten könnte, sie als gelöscht zu definieren. Eine Funktion wird vom Benutzer bereitgestellt, wenn sie vom Benutzer deklariert und bei ihrer ersten Deklaration nicht explizit als Standard festgelegt oder gelöscht wurde.Eine vom Benutzer bereitgestellte explizit voreingestellte Funktion (dh explizit voreingestellte Funktion nach ihrer ersten Deklaration) wird an dem Punkt definiert, an dem sie explizit voreingestellt ist. Wenn eine solche Funktion implizit als gelöscht definiert ist, ist das Programm fehlerhaft. [Hinweis: Das Deklarieren einer Funktion als Standard nach ihrer ersten Deklaration kann eine effiziente Ausführung und präzise Definition ermöglichen und gleichzeitig eine stabile binäre Schnittstelle zu einer sich entwickelnden Codebasis ermöglichen. - Endnote]

Betonung meiner

Wir können also sehen, dass bar()es jetzt als vom Benutzer bereitgestellt gilt , da Sie es bei der ersten Deklaration nicht als Standard festgelegt haben . Aus diesem Grund [dcl.init] /8.2

Wenn T ein (möglicherweise lebenslaufqualifizierter) Klassentyp ohne einen vom Benutzer bereitgestellten oder gelöschten Standardkonstruktor ist, wird das Objekt mit Null initialisiert und die semantischen Einschränkungen für die Standardinitialisierung werden überprüft. Wenn T einen nicht trivialen Standardkonstruktor hat wird das Objekt standardmäßig initialisiert;

gilt nicht mehr und wir sind keine Wertinitialisierung,b sondern eine Standardinitialisierung gemäß [dcl.init] /8.1

Wenn T ein (möglicherweise cv-qualifizierter) Klassentyp ([class]) ohne Standardkonstruktor ([class.default.ctor]) oder ein vom Benutzer bereitgestellter oder gelöschter Standardkonstruktor ist, wird das Objekt standardmäßig initialisiert ;;

NathanOliver
quelle
52
Ich meine (*_*)... Wenn ich überhaupt die Grundkonstrukte der Sprache verwenden will, muss ich das Kleingedruckte des Sprachentwurfs lesen, dann Halleluja! Aber es scheint wahrscheinlich das zu sein, was Sie sagen.
Duck Dodgers
12
@balki Ja, Outline bar::bar() = defaultist das Gleiche wie bar::bar(){}Inline.
NathanOliver
15
@JoeyMallone Ja, C ++ kann ziemlich kompliziert sein. Ich bin mir nicht sicher, was der Grund dafür ist.
NathanOliver
3
Wenn es eine vorherige Deklaration gibt, initialisiert eine nachfolgende Definition mit dem Standardschlüsselwort die Mitglieder NICHT auf Null. Richtig? Das ist richtig. Es ist das, was hier passiert.
NathanOliver
6
Der Grund ist genau dort in Ihrem Zitat: Der Punkt eines Out-of-Line-Standardwerts besteht darin, "eine effiziente Ausführung und präzise Definition bereitzustellen und gleichzeitig eine stabile binäre Schnittstelle zu einer sich entwickelnden Codebasis zu ermöglichen", mit anderen Worten, Sie können zu wechseln eine vom Benutzer geschriebene Stelle später, falls erforderlich, ohne ABI zu beschädigen. Beachten Sie, dass die Out-of-Line-Definition nicht implizit inline ist und daher standardmäßig nur in einer TU angezeigt werden kann. Eine andere TU, die nur die Klassendefinition sieht, kann nicht wissen, ob sie explizit als Standard definiert ist.
TC
25

Der Unterschied im Verhalten resultiert aus der Tatsache , dass nach [dcl.fct.def.default]/5, bar::barist vom Benutzer bereitgestellte , wo foo::fookeine 1 . Infolgedessen foo::foowerden die Mitglieder wertinitialisiert (dh nullinitialisiert foo::a ), bar::barbleiben jedoch nicht initialisiert 2 .


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

Eine Funktion wird vom Benutzer bereitgestellt, wenn sie vom Benutzer deklariert und bei ihrer ersten Deklaration nicht explizit als Standard festgelegt oder gelöscht wurde.

2)

Aus [dcl.init # 6] :

Ein Objekt vom Typ T wertinitialisieren bedeutet:

  • Wenn T ein (möglicherweise lebenslaufqualifizierter) Klassentyp ohne Standardkonstruktor ([class.ctor]) oder einen vom Benutzer bereitgestellten oder gelöschten Standardkonstruktor ist, wird das Objekt standardmäßig initialisiert.

  • Wenn T ein (möglicherweise lebenslaufqualifizierter) Klassentyp ohne einen vom Benutzer bereitgestellten oder gelöschten Standardkonstruktor ist, wird das Objekt mit Null initialisiert und die semantischen Einschränkungen für die Standardinitialisierung werden überprüft. Wenn T einen nicht trivialen Standardkonstruktor hat wird das Objekt standardmäßig initialisiert;

  • ...

Aus [dcl.init.list] :

Die Listeninitialisierung eines Objekts oder einer Referenz vom Typ T ist wie folgt definiert:

  • ...

  • Andernfalls wird das Objekt wertinitialisiert, wenn die Initialisierungsliste keine Elemente enthält und T ein Klassentyp mit einem Standardkonstruktor ist.

Aus der Antwort von Vittorio Romeo

YSC
quelle
10

Aus der Referenz :

Die Aggregatinitialisierung initialisiert Aggregate. Es ist eine Form der Listeninitialisierung.

Ein Aggregat ist einer der folgenden Typen:

[snip]

  • Klassentyp [snip], der hat

    • [snip] (es gibt Variationen für verschiedene Standardversionen)

    • Keine vom Benutzer bereitgestellten, geerbten oder expliziten Konstruktoren (explizit standardmäßige oder gelöschte Konstruktoren sind zulässig).

    • [snip] (es gibt mehr Regeln, die für beide Klassen gelten)

Bei dieser Definition foohandelt es sich um ein Aggregat, während dies barnicht der Fall ist (es verfügt über einen vom Benutzer bereitgestellten, nicht standardmäßigen Konstruktor).

Daher wird für foo, T object {arg1, arg2, ...};wird für die aggregierte Initialisierung Syntax.

Die Auswirkungen der Aggregatinitialisierung sind:

  • [snip] (einige Details sind für diesen Fall irrelevant)

  • Wenn die Anzahl der Initialisierungsklauseln geringer ist als die Anzahl der Mitglieder oder die Initialisierungsliste vollständig leer ist, werden die verbleibenden Mitglieder wertinitialisiert .

Daher a.awird der Wert initialisiert, was intbedeutet, dass keine Initialisierung erfolgt.

Für bar, T object {};auf der anderen Seite ist Wert Initialisierung (der Klasseninstanz, nicht Wert Initialisierung der Mitglieder!). Da es sich um einen Klassentyp mit einem Standardkonstruktor handelt, wird der Standardkonstruktor aufgerufen. Der von Ihnen definierte Standardkonstruktor initialisiert die Mitglieder (da keine Elementinitialisierer vorhanden sind), die im Fall von int(mit nicht statischem Speicher) b.beinen unbestimmten Wert hinterlassen.

Bei Pod-Typen ist die Standardinitialisierung die Nullinitialisierung.

Nein, das ist falsch.


PS Ein Wort zu Ihrem Experiment und Ihrer Schlussfolgerung: Wenn Sie sehen, dass die Ausgabe Null ist, bedeutet dies nicht unbedingt, dass die Variable mit Null initialisiert wurde. Null ist eine durchaus mögliche Zahl für einen Müllwert.

dafür habe ich das programm vielleicht 5 ~ 6 mal vor dem posten ausgeführt und jetzt ungefähr 10 mal, a ist immer null. b ändert sich ein wenig.

Die Tatsache, dass der Wert mehrmals gleich war, bedeutet nicht unbedingt, dass er auch initialisiert wurde.

Ich habe es auch mit set (CMAKE_CXX_STANDARD 14) versucht. Das Ergebnis war das gleiche.

Die Tatsache, dass das Ergebnis bei mehreren Compileroptionen gleich ist, bedeutet nicht, dass die Variable initialisiert wird. (Obwohl in einigen Fällen das Ändern der Standardversion ändern kann, ob sie initialisiert wird).

Wie könnte ich meinen RAM ein wenig schütteln, damit es jetzt etwas anderes sein sollte, wenn dort Null wäre?

In C ++ gibt es keine garantierte Möglichkeit, einen nicht initialisierten Wert ungleich Null anzuzeigen.

Die einzige Möglichkeit zu wissen, dass eine Variable initialisiert wird, besteht darin, das Programm mit den Regeln der Sprache zu vergleichen und zu überprüfen, ob die Regeln besagen, dass es initialisiert ist. In diesem Fall a.awird tatsächlich initialisiert.

Eerorika
quelle
"Der von Ihnen definierte Standardkonstruktor initialisiert die Mitglieder (da keine Elementinitialisierer vorhanden sind), wodurch im Fall von int ein unbestimmter Wert erhalten bleibt." -> eh! "Für Pod-Typen ist die Standardinitialisierung die Nullinitialisierung." oder liege ich falsch?
Duck Dodgers
2
@JoeyMallone Die Standardinitialisierung von POD-Typen ist keine Initialisierung.
NathanOliver
@ NathanOliver, dann bin ich noch verwirrter. Wie kommt es dann, awird initialisiert. Ich dachte, adie Standardinitialisierung und die Standardinitialisierung für einen Mitglieds-POD ist die Nullinitialisierung. Kommt adann zum Glück immer auf Null, egal wie oft ich dieses Programm starte.
Duck Dodgers
@JoeyMallone Then how come a is initialized.Da es sich um einen initialisierten Wert handelt. I was thinking a is default initializedEs ist nicht.
Eerorika
3
@JoeyMallone Mach dir keine Sorgen. Sie könnten aus der Initialisierung in C ++ ein Buch machen. Wenn Sie eine Chance bekommen, hat CppCon auf youtube ein paar Videos zur Initialisierung, wobei die enttäuschendsten (wie beim Hinweis darauf, wie schlimm es ist) youtube.com/watch?v=7DTlWPgX6zs
NathanOliver
0

Meh, ich habe versucht, das von Ihnen bereitgestellte Snippet test.cppüber gcc & clang und mehrere Optimierungsstufen auszuführen:

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 ]

Hier wird es also interessant. Es zeigt deutlich, dass der Clang-O0-Build Zufallszahlen liest, vermutlich Stapelspeicher.

Ich drehte schnell meine IDA auf, um zu sehen, was passiert:

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;
}

Was bar::bar(bar *this)macht nun?

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

Hmm, nichts. Wir mussten auf die Montage zurückgreifen:

.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

Also ja, es ist einfach nichts, was der Konstruktor im Grunde tut this = this. Wir wissen jedoch, dass tatsächlich zufällige nicht initialisierte Stapeladressen geladen und gedruckt werden.

Was ist, wenn wir explizit Werte für die beiden Strukturen angeben?

#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 ]

Ähnliches Schicksal auch bei g ++:

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 ]

Dies bedeutet also, dass es sich effektiv um eine direkte Initialisierung handelt bar b(0), nicht um eine aggregierte Initialisierung.

Dies liegt wahrscheinlich daran, dass dies möglicherweise ein externes Symbol sein kann, wenn Sie keine explizite Konstruktorimplementierung bereitstellen, z.

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

Der Compiler ist nicht klug genug, um dies in einer nicht optimierten Phase als No-Op / Inline-Aufruf abzuleiten.

Steve Fan
quelle