Inizializzazione zero C ++ - Perché `b` in questo programma non è inizializzato, ma` a` è inizializzato?


135

In base alla risposta accettata (e unica) per questa domanda StackTranslate.it ,

Definire il costruttore con

MyTest() = default;

inizializzerà invece zero l'oggetto.

Allora perché fa quanto segue,

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

produrre questo output:

0 32766

Entrambi i costruttori definiti sono predefiniti? Destra? E per i tipi POD, l'inizializzazione predefinita è zero-inizializzazione.

E secondo la risposta accettata per questa domanda ,

  1. Se un membro POD non è inizializzato nel costruttore né tramite l'inizializzazione in classe C ++ 11, viene inizializzato per impostazione predefinita.

  2. La risposta è la stessa indipendentemente dallo stack o dall'heap.

  3. In C ++ 98 (e non in seguito), è stato specificato new int () come eseguendo l'inizializzazione zero.

Nonostante abbia cercato di avvolgere la mia testa (anche se minuscola ) attorno ai costruttori predefiniti e all'inizializzazione predefinita , non sono riuscito a trovare una spiegazione.


3
È interessante notare che ricevo anche un avviso per b: main.cpp: 18: 34: avviso: 'b.bar::b' viene utilizzato non inizializzato in questa funzione [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl

8
barIl costruttore è fornito dall'utente mentre fooil costruttore è quello predefinito.
Jarod42,

2
@PeteBecker, lo capisco. Come potrei in qualche modo scuotere un po 'la mia RAM in modo che se ci fosse zero lì, ora dovrebbe essere qualcos'altro. ;) ps ho eseguito il programma una dozzina di volte. Non è un grande programma. Potresti eseguirlo e testarlo sul tuo sistema. aè zero. bnon è. Sembra ainizializzato.
Duck Dodgers,

2
@JoeyMallone Per quanto riguarda "come viene fornito dall'utente": non vi è alcuna garanzia che la definizione di bar::bar()sia visibile in main()- potrebbe essere definita in un'unità di compilazione separata e fare qualcosa di molto non banale mentre in main()solo la dichiarazione è visibile. Penso che accetti che questo comportamento non dovrebbe cambiare a seconda che tu inserisca bar::bar()la definizione in un'unità di compilazione separata o meno (anche se l'intera situazione non è intuitiva).
Max Langhof,

2
@balki O int a = 0;vuoi essere davvero esplicito.
NathanOliver,

Risposte:


109

Il problema qui è piuttosto sottile. Lo penseresti

bar::bar() = default;

ti darebbe un costruttore predefinito generato dal compilatore, e lo fa, ma ora è considerato fornito dall'utente. [dcl.fct.def.default] / 5 afferma:

Le funzioni esplicitamente predefinite e le funzioni dichiarate implicitamente sono chiamate collettivamente funzioni predefinite e l'implementazione fornirà loro definizioni implicite ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign ]), che potrebbe significare definirli come eliminati. Una funzione viene fornita dall'utente se viene dichiarata dall'utente e non viene esplicitamente modificata o eliminata nella sua prima dichiarazione.Una funzione fornita in modo esplicito di default dall'utente (ovvero, esplicitamente predefinita dopo la sua prima dichiarazione) è definita nel punto in cui è esplicitamente predefinita; se tale funzione è implicitamente definita come eliminata, il programma è mal formato. [Nota: la dichiarazione di una funzione come predefinita dopo la sua prima dichiarazione può fornire un'esecuzione efficiente e una definizione concisa, consentendo al contempo un'interfaccia binaria stabile a una base di codice in evoluzione. - nota finale]

enfatizzare il mio

Quindi possiamo vedere che dal momento che non lo bar()hai impostato di default quando lo hai dichiarato per la prima volta, ora è considerato fornito dall'utente. Per questo motivo [dcl.init] /8.2

se T è un tipo di classe (possibilmente qualificato in cv) senza un costruttore predefinito fornito dall'utente o eliminato, l'oggetto viene inizializzato con zero e vengono controllati i vincoli semantici per l'inizializzazione predefinita e se T ha un costruttore predefinito non banale , l'oggetto è inizializzato per impostazione predefinita;

non si applica più e non stiamo inizializzando il valore bma inizializzandolo come predefinito per [dcl.init] /8.1

se T è un tipo di classe (possibilmente qualificato in cv) ([classe]) senza costruttore predefinito ([class.default.ctor]) o costruttore predefinito fornito dall'utente o eliminato, l'oggetto viene inizializzato come predefinito ;


52
Voglio dire (*_*).... Se anche per usare i costrutti di base della lingua, devo leggere la stampa fine della bozza della lingua, allora Alleluia! Ma probabilmente sembra essere quello che dici.
Duck Dodgers,

12
@balki Sì, fare bar::bar() = defaultfuori linea è come fare in bar::bar(){}linea.
NathanOliver,

15
@JoeyMallone Sì, il C ++ può essere piuttosto complicato. Non sono sicuro di quale sia la ragione.
NathanOliver,

3
Se esiste una dichiarazione precedente, una definizione successiva con la parola chiave predefinita NON azzererà i membri. Destra? Questo è corretto. È quello che sta succedendo qui.
NathanOliver,

6
Il motivo è proprio qui nel tuo preventivo: il punto di un default fuori linea è "fornire un'esecuzione efficiente e una definizione concisa, consentendo al contempo un'interfaccia binaria stabile a una base di codice in evoluzione", in altre parole, ti consente di passare a un corpo scritto dall'utente in seguito, se necessario, senza interrompere l'ABI. Si noti che la definizione fuori linea non è implicitamente in linea e quindi può apparire solo in una TU per impostazione predefinita; un'altra TU che vede da sola la definizione della classe non ha modo di sapere se è esplicitamente definita come predefinita.
TC

25

La differenza di comportamento deriva dal fatto che, secondo [dcl.fct.def.default]/5, bar::barè fornito dall'utente dove foo::foonon è 1 . Di conseguenza, foo::foosarà valutare inizializzare suoi componenti (cioè: zero di inizializzazione foo::a ) ma bar::barrimanga costantemente inizializzato 2 .


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

Una funzione viene fornita dall'utente se viene dichiarata dall'utente e non viene esplicitamente modificata o eliminata nella sua prima dichiarazione.

2)

Da [dcl.init # 6] :

Inizializzare un oggetto di tipo T significa:

  • se T è un tipo di classe (possibilmente qualificato in cv) senza costruttore predefinito ([class.ctor]) o costruttore predefinito fornito dall'utente o eliminato, l'oggetto viene inizializzato come predefinito;

  • se T è un tipo di classe (possibilmente qualificato in cv) senza un costruttore predefinito fornito dall'utente o eliminato, quindi l'oggetto viene inizializzato a zero e vengono controllati i vincoli semantici per l'inizializzazione predefinita e se T ha un costruttore predefinito non banale , l'oggetto è inizializzato per impostazione predefinita;

  • ...

Da [dcl.init.list] :

L'inizializzazione dell'elenco di un oggetto o un riferimento di tipo T è definita come segue:

  • ...

  • Altrimenti, se l'elenco di inizializzatori non ha elementi e T è un tipo di classe con un costruttore predefinito, l'oggetto viene inizializzato dal valore.

Dalla risposta di Vittorio Romeo


10

Da cppreference :

L'inizializzazione aggregata inizializza gli aggregati. È una forma di inizializzazione dell'elenco.

Un aggregato è uno dei seguenti tipi:

[Omissis]

  • tipo di classe [snip], ovvero

    • [snip] (esistono varianti per diverse versioni standard)

    • nessun costruttore fornito dall'utente, ereditato o esplicito (sono ammessi costruttori esplicitamente predefiniti o eliminati)

    • [snip] (ci sono più regole, che si applicano ad entrambe le classi)

Data questa definizione, fooè un aggregato, mentre barnon lo è (ha un costruttore non predefinito fornito dall'utente).

Pertanto foo, T object {arg1, arg2, ...};è sintassi per l'inizializzazione aggregata.

Gli effetti dell'inizializzazione aggregata sono:

  • [snip] (alcuni dettagli non pertinenti a questo caso)

  • Se il numero di clausole di inizializzazione è inferiore al numero di membri o l'elenco di inizializzatori è completamente vuoto, i membri rimanenti vengono inizializzati dal valore .

Pertanto a.aviene inizializzato il valore, il che intsignifica azzerare l'inizializzazione.

Per bar, T object {};d'altra parte è il valore di inizializzazione (dell'istanza di classe, non il valore di inizializzazione di membri!). Poiché si tratta di un tipo di classe con un costruttore predefinito, viene chiamato il costruttore predefinito. Il costruttore predefinito che hai definito predefinito inizializza i membri (in virtù del fatto che non hanno inizializzatori di membri), che in caso di int(con memoria non statica) lascia b.bun valore indeterminato.

E per i tipi di pod, l'inizializzazione predefinita è zero-inizializzazione.

No. Questo è sbagliato.


PS Una parola sul tuo esperimento e la tua conclusione: vedere che l'output è zero non significa necessariamente che la variabile era zero inizializzata. Zero è un numero perfettamente possibile per un valore di immondizia.

per questo ho eseguito il programma forse 5 ~ 6 volte prima di pubblicare e circa 10 volte ora, a è sempre zero. b cambia leggermente.

Il fatto che il valore sia stato lo stesso più volte non significa necessariamente che sia stato inizializzato.

Ho anche provato con set (CMAKE_CXX_STANDARD 14). Il risultato è stato lo stesso.

Il fatto che il risultato sia lo stesso con più opzioni del compilatore non significa che la variabile sia inizializzata. (Anche se in alcuni casi, la modifica della versione standard può cambiare se viene inizializzata).

Come potrei in qualche modo scuotere un po 'la mia RAM in modo che se ci fosse zero lì, ora dovrebbe essere qualcos'altro

Non esiste un modo garantito in C ++ per rendere il valore del valore non inizializzato in modo che appaia diverso da zero.

L'unico modo per sapere che una variabile è inizializzata è confrontare il programma con le regole della lingua e verificare che le regole dicano che è inizializzata. In questo caso a.aviene infatti inizializzato.


"Il costruttore predefinito che hai definito predefinito inizializza i membri (in virtù del fatto che non hanno inizializzatori di membri), che in caso di int lo lascia con un valore indeterminato." -> eh! "per i tipi di pod, l'inizializzazione predefinita è zero-inizializzazione." o mi sbaglio?
Duck Dodgers,

2
@JoeyMallone L'inizializzazione predefinita dei tipi POD non è inizializzazione.
NathanOliver,

@NathanOliver, allora sono ancora più confuso. Quindi come mai aviene inizializzato. Stavo pensando che l' ainizializzazione predefinita è l'inizializzazione predefinita di un membro POD, l'inizializzazione zero. È aquindi solo per fortuna sempre in arrivo pari a zero, non importa quante volte ho eseguito questo programma.
Duck Dodgers,

@JoeyMallone Then how come a is initialized.Perché è un valore inizializzato. I was thinking a is default initializedNon è.
eerorika,

3
@JoeyMallone Non preoccuparti. È possibile creare un libro dall'inizializzazione in C ++. Se hai la possibilità che CppCon su youtube abbia alcuni video sull'inizializzazione con il più deludente (come nel sottolineare quanto sia male) essere youtube.com/watch?v=7DTlWPgX6zs
NathanOliver

0

Ho provato a eseguire lo snippet che hai fornito test.cpptramite gcc & clang e livelli di ottimizzazione multipli:

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 ]

Quindi è lì che diventa interessante, mostra chiaramente che il clangore O0 build sta leggendo numeri casuali, presumibilmente stack space.

Ho rapidamente attivato il mio IDA per vedere cosa sta succedendo:

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

Ora cosa bar::bar(bar *this)fa?

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

Hmm, niente. Abbiamo dovuto ricorrere all'uso di assembly:

.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

Quindi sì, è proprio niente, ciò che il costruttore fondamentalmente fa è this = this. Ma sappiamo che sta effettivamente caricando indirizzi stack casuali non inizializzati e stampandolo.

Cosa succede se forniamo esplicitamente valori per le due strutture?

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

Destino simile anche con 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 ]

Quindi questo significa che è effettivamente un'inizializzazione diretta bar b(0), non un'inizializzazione aggregata.

Ciò è probabilmente dovuto al fatto che se non si fornisce un'implementazione esplicita del costruttore questo potrebbe essere potenzialmente un simbolo esterno, ad esempio:

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

Il compilatore non è abbastanza intelligente da dedurlo come una chiamata no-op / inline in una fase non ottimizzata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.