Inicjalizacja C ++ zero - dlaczego „b” w tym programie jest niezainicjalizowane, ale „a” jest zainicjowane?

135

Zgodnie z zaakceptowaną (i jedyną) odpowiedzią na to pytanie dotyczące przepełnienia stosu ,

Definiowanie konstruktora za pomocą

MyTest() = default;

zamiast tego wyzeruje obiekt.

W takim razie dlaczego następujące

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

wygeneruj ten wynik:

0 32766

Oba zdefiniowane konstruktory są domyślne? Dobrze? W przypadku typów POD domyślną inicjalizacją jest inicjalizacja zerowa.

I zgodnie z przyjętą odpowiedzi na to pytanie ,

  1. Jeśli element członkowski POD nie zostanie zainicjowany w konstruktorze ani za pośrednictwem inicjalizacji w klasie C ++ 11, jest inicjowany domyślnie.

  2. Odpowiedź jest taka sama, niezależnie od stosu lub stosu.

  3. W C ++ 98 (i nie później), new int () zostało określone jako wykonujące zerową inicjalizację.

Pomimo próby owinięcia mojej (choć malutkiej ) głowy wokół domyślnych konstruktorów i domyślnej inicjalizacji , nie mogłem wymyślić wyjaśnienia.

Duck Dodgers
źródło
3
Co ciekawe, dostaję nawet ostrzeżenie o b: main.cpp: 18: 34: warning: 'b.bar::b' jest używane niezainicjalizowane w tej funkcji [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl
8
barKonstruktor jest dostarczany przez użytkownika, podczas gdy fookonstruktor jest konstruktorem domyślnym.
Jarod42
2
@PeteBecker, rozumiem to. Jak mogłem w jakiś sposób potrząsnąć trochę pamięcią RAM, aby jeśli było tam zero, teraz powinno być coś innego. ;) ps Uruchomiłem program kilkanaście razy. To nie jest duży program. Możesz go uruchomić i przetestować w swoim systemie. awynosi zero. bnie jest. Wydaje się, że ajest zainicjowany.
Duck Dodgers
2
@JoeyMallone Odnośnie „jak to jest podane przez użytkownika”: Nie ma gwarancji, że definicja bar::bar()jest widoczna w main()- może być zdefiniowana w oddzielnej jednostce kompilacji i zrobić coś bardzo nietrywialnego, podczas gdy main()widoczna jest tylko deklaracja. Myślę, że zgodzisz się, że to zachowanie nie powinno się zmieniać w zależności od tego, czy bar::bar()umieścisz definicję w oddzielnej jednostce kompilacji, czy nie (nawet jeśli cała sytuacja jest nieintuicyjna).
Max Langhof
2
@balki A int a = 0;może chcesz być naprawdę wyraźny.
NathanOliver

Odpowiedzi:

109

Problem jest tutaj dość subtelny. Można by tak pomyśleć

bar::bar() = default;

dałoby ci domyślny konstruktor wygenerowany przez kompilator i robi, ale teraz jest uważany za dostarczony przez użytkownika. [dcl.fct.def.default] / 5 stwierdza:

Funkcje z jawnie ustawionymi domyślnymi i niejawnie zadeklarowanymi funkcjami są zbiorczo nazywane funkcjami domyślnymi, a implementacja powinna zapewnić ich niejawne definicje ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy. assign ]), co może oznaczać zdefiniowanie ich jako usuniętych. Funkcja jest dostarczana przez użytkownika, jeśli została zadeklarowana przez użytkownika i nie została jawnie ustawiona jako domyślna ani usunięta przy pierwszej deklaracji.Podana przez użytkownika funkcja z jawnie domyślnymi ustawieniami (tj. Jawnie ustawiona jako domyślna po jej pierwszej deklaracji) jest definiowana w miejscu, w którym została jawnie ustawiona jako domyślna; jeśli taka funkcja jest niejawnie zdefiniowana jako usunięta, program jest nieprawidłowo sformułowany. [Uwaga: Zadeklarowanie funkcji jako domyślnej po jej pierwszej deklaracji może zapewnić wydajne wykonanie i zwięzłą definicję, jednocześnie umożliwiając stabilny interfejs binarny dla ewoluującej bazy kodu. - notatka końcowa]

podkreślenie moje

Widzimy więc, że skoro nie ustawiłeś wartości domyślnej, bar()gdy po raz pierwszy ją zadeklarowałeś, jest teraz uważany za dostarczony przez użytkownika. Z tego powodu [dcl.init] /8.2

jeśli T jest typem klasy (prawdopodobnie kwalifikowanym przez cv) bez konstruktora domyślnego dostarczonego przez użytkownika lub usuniętego, to obiekt jest inicjowany przez zero, a ograniczenia semantyczne dla inicjalizacji domyślnej są sprawdzane, a jeśli T ma nietrywialny konstruktor domyślny , obiekt jest inicjalizowany domyślnie;

już nie ma zastosowania i nie inicjalizujemy wartości, bale zamiast tego domyślnie inicjalizujemy ją zgodnie z [dcl.init] /8.1

jeśli T jest (prawdopodobnie kwalifikowaną przez cv) typem klasy ([klasa]) bez domyślnego konstruktora ([class.default.ctor]) lub domyślnego konstruktora dostarczonego lub usuniętego przez użytkownika, to obiekt jest inicjowany domyślnie ;

NathanOliver
źródło
52
Mam na myśli (*_*)... Jeśli nawet użyję podstawowych konstrukcji języka, muszę przeczytać drobnym drukiem szkicu języka, a następnie Alleluja! Ale prawdopodobnie wydaje się, że tak właśnie mówisz.
Duck Dodgers
12
@balki Tak, bar::bar() = defaultwyjście poza linię działa tak samo, jak wykonanie bar::bar(){}inline.
NathanOliver
15
@JoeyMallone Tak, C ++ może być dość skomplikowane. Nie jestem pewien, jaki jest tego powód.
NathanOliver
3
Jeśli istnieje poprzednia deklaracja, to następna definicja z domyślnym słowem kluczowym NIE spowoduje zerowej inicjalizacji członków. Dobrze? To jest poprawne. To właśnie się tutaj dzieje.
NathanOliver
6
Powód znajduje się w Twojej cytacie: celem domyślnej wersji poza linią jest „zapewnienie wydajnego wykonywania i zwięzłej definicji przy jednoczesnym umożliwieniu stabilnego interfejsu binarnego dla rozwijającej się bazy kodu”, innymi słowy, umożliwienie przełączenia się na treść napisana przez użytkownika później, jeśli to konieczne, bez przerywania ABI. Zwróć uwagę, że definicja poza wierszem nie jest niejawnie wbudowana, więc domyślnie może pojawić się tylko w jednej jednostce tłumaczeniowej; inna jednostka organizacyjna, która widzi samą definicję klasy, nie ma możliwości sprawdzenia, czy jest ona jawnie zdefiniowana jako domyślna.
TC
25

Różnica w zachowaniu wynika z faktu, że, zgodnie z [dcl.fct.def.default]/5, bar::barjest dostarczane przez użytkownika, gdzie foo::foonie jest 1 . W konsekwencji foo::foobędzie cenią zainicjować swoich członków (czyli: zero-zainicjować foo::a ), ale bar::barpozostanie niezainicjowanymi 2 .


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

Funkcja jest dostarczana przez użytkownika, jeśli została zadeklarowana przez użytkownika i nie została jawnie ustawiona jako domyślna ani usunięta przy pierwszej deklaracji.

2)

Od [dcl.init # 6] :

Inicjalizacja wartości obiektu typu T oznacza:

  • jeśli T jest typem klasy (prawdopodobnie kwalifikowanym przez cv) bez domyślnego konstruktora ([class.ctor]) lub domyślnego konstruktora dostarczonego lub usuniętego przez użytkownika, to obiekt jest inicjowany domyślnie;

  • jeśli T jest typem klasy (prawdopodobnie kwalifikowanym przez cv) bez konstruktora domyślnego dostarczonego przez użytkownika lub usuniętego, to obiekt jest inicjowany zerem i sprawdzane są ograniczenia semantyczne dla inicjalizacji domyślnej, a jeśli T ma nietrywialny konstruktor domyślny obiekt jest inicjalizowany domyślnie;

  • ...

Z [dcl.init.list] :

Inicjalizacja listy obiektu lub odwołania typu T jest zdefiniowana w następujący sposób:

  • ...

  • W przeciwnym razie, jeśli lista inicjalizacyjna nie zawiera elementów, a T jest typem klasy z domyślnym konstruktorem, obiekt jest inicjowany wartością.

Z odpowiedzi Vittorio Romeo

YSC
źródło
10

Od cppreference :

Inicjalizacja agregatu inicjuje agregacje. Jest to forma inicjalizacji listy.

Agregat to jeden z następujących typów:

[fantastyczna okazja]

  • typ klasy [wycinek], który ma

    • [wycinek] (istnieją odmiany dla różnych wersji standardowych)

    • żadnych konstruktorów dostarczonych przez użytkownika, dziedziczonych ani jawnych (jawnie domyślne lub usunięte konstruktory są dozwolone)

    • [wycinek] (jest więcej reguł, które dotyczą obu klas)

Biorąc pod uwagę tę definicję, foojest agregatem, abar nie (ma dostarczony przez użytkownika, niedomyślny konstruktor).

Dlatego dla foo, T object {arg1, arg2, ...};jest składnia do inicjalizacji agregacji.

Efekty inicjalizacji agregatu to:

  • [wycinek] (niektóre szczegóły nie mają znaczenia w tym przypadku)

  • Jeśli liczba klauzul inicjujących jest mniejsza niż liczba elementów członkowskich lub lista inicjatorów jest całkowicie pusta, pozostałe elementy członkowskie są inicjowane z wartością .

Dlatego a.awartość jest inicjalizowana, co intoznacza zerową inicjalizację.

Na bar, T object {};z drugiej strony jest inicjalizacji wartość (na przykład klasy, a nie wartość inicjalizacji członków!). Ponieważ jest to typ klasy z domyślnym konstruktorem, wywoływany jest domyślny konstruktor. Domyślny konstruktor, który zdefiniowałeś domyślnie, inicjuje elementy członkowskie (z powodu braku inicjatorów intskładowych) , które w przypadku (z niestatyczną pamięcią) pozostawiają b.bnieokreśloną wartość.

W przypadku typów pod domyślną inicjalizacją jest inicjalizacja zerowa.

Nie. To jest złe.


PS Kilka słów na temat twojego eksperymentu i twojego wniosku: Widzenie, że wynik ma wartość zero, niekoniecznie oznacza, że ​​zmienna została zainicjalizowana zero. Zero jest jak najbardziej możliwą liczbą dla wartości śmieciowej.

w tym celu uruchomiłem program może 5 ~ 6 razy przed wysłaniem i około 10 razy teraz, a wynosi zawsze zero. b trochę się zmienia.

Fakt, że wartość była wielokrotnie taka sama, niekoniecznie oznacza, że ​​została również zainicjowana.

Próbowałem też z zestawem (CMAKE_CXX_STANDARD 14). Wynik był taki sam.

Fakt, że wynik jest taki sam w przypadku wielu opcji kompilatora, nie oznacza, że ​​zmienna została zainicjalizowana. (Chociaż w niektórych przypadkach zmiana wersji standardowej może zmienić, czy jest ona zainicjowana).

Jak mogłem w jakiś sposób potrząsnąć trochę moją pamięcią RAM, aby jeśli było tam zero, teraz powinno być coś innego

W C ++ nie ma żadnego gwarantowanego sposobu, aby niezainicjowana wartość wyglądała na niezerową.

Jedynym sposobem, aby dowiedzieć się, że zmienna jest zainicjalizowana, jest porównanie programu z regułami języka i sprawdzenie, czy reguły mówią, że została zainicjalizowana. W tym przypadku a.ajest rzeczywiście inicjalizowany.

eerorika
źródło
„Domyślny konstruktor, który zdefiniowałeś default, inicjuje składowe (ze względu na brak inicjalizatorów składowych), co w przypadku int pozostawia nieokreśloną wartość”. -> eh! „W przypadku typów podów domyślna inicjalizacja to inicjalizacja zerowa”. czy się mylę?
Duck Dodgers
2
@JoeyMallone Domyślna inicjalizacja typów POD to brak inicjalizacji.
NathanOliver
@NathanOliver, Wtedy jestem jeszcze bardziej zdezorientowany. Następnie ainicjalizowane jest jak to się stało . Myślałem, że ajest domyślnie zainicjowany, a domyślna inicjalizacja dla członka POD to inicjalizacja zerowa. Na aszczęście zawsze zbliża się do zera, bez względu na to, ile razy uruchamiam ten program.
Duck Dodgers
@JoeyMallone Then how come a is initialized.Ponieważ jest zainicjowana wartość. I was thinking a is default initializedNie jest.
eerorika
3
@JoeyMallone Nie martw się o to. Możesz zrobić książkę z inicjalizacji w C ++. Jeśli masz szansę, CppCon na youtube zawiera kilka filmów na temat inicjalizacji, z których najbardziej rozczarowujące (jak wskazanie, jak źle jest) jest youtube.com/watch?v=7DTlWPgX6zs
NathanOliver
0

Ech, próbowałem uruchomić fragment test.cppkodu, który podałeś jako , poprzez gcc i clang oraz wiele poziomów optymalizacji:

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 ]

Więc to jest tam, gdzie robi się interesująco, wyraźnie pokazuje, że clang O0 build odczytuje liczby losowe, prawdopodobnie miejsce na stosie.

Szybko włączyłem IDA, żeby zobaczyć, co się dzieje:

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

Co teraz bar::bar(bar *this)robi?

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

Hmm, nic. Musieliśmy uciekać się do montażu:

.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

Więc tak, to po prostu nic, co zasadniczo robi konstruktor this = this. Ale wiemy, że w rzeczywistości ładuje losowe niezainicjowane adresy stosu i drukuje je.

Co się stanie, jeśli jawnie podamy wartości dla dwóch struktur?

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

Uderz, brzęk, 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 ]

Podobny los również z 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 ]

Oznacza to, że jest to faktycznie inicjalizacja bezpośrednia bar b(0), a nie inicjalizacja zbiorcza.

Dzieje się tak prawdopodobnie dlatego, że jeśli nie podasz jawnej implementacji konstruktora, może to być symbol zewnętrzny, na przykład:

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

Kompilator nie jest wystarczająco inteligentny, aby wydedukować to jako wywołanie bez operacji / wbudowane w niezoptymalizowanym etapie.

Steve Fan
źródło