Default, valore e zero pasticcio di inizializzazione


88

Sono molto confuso sull'inizializzazione di valori e valori predefiniti e zero. e soprattutto quando entrano in gioco per i diversi standard C ++ 03 e C ++ 11 (e C ++ 14 ).

Sto citando e cercando di estendere una risposta davvero buona Value- / Default- / Zero- Init C ++ 98 e C ++ 03 qui per renderlo più generale in quanto aiuterebbe molti utenti se qualcuno potesse aiutare a compilare il aveva bisogno di lacune per avere una buona panoramica su cosa succede e quando?

La visione completa degli esempi in poche parole:

A volte la memoria restituita dal nuovo operatore verrà inizializzata, ea volte non dipenderà dal fatto che il tipo che stai facendo di nuovo sia un POD (semplici dati vecchi) , o se è una classe che contiene membri POD e sta usando un costruttore predefinito generato dal compilatore.

  • In C ++ 1998 ci sono 2 tipi di inizializzazione: zero e inizializzazione predefinita
  • In C ++ 2003 è stato aggiunto un terzo tipo di inizializzazione, l'inizializzazione del valore .
  • In C ++ 2011 / C ++ 2014 è stata aggiunta solo l' inizializzazione dell'elenco e le regole per l' inizializzazione valore / predefinito / zero sono state leggermente modificate.

Assumere:

struct A { int m; };                     
struct B { ~B(); int m; };               
struct C { C() : m(){}; ~C(); int m; };  
struct D { D(){}; int m; };             
struct E { E() = default; int m;}; /** only possible in c++11/14 */  
struct F {F(); int m;};  F::F() = default; /** only possible in c++11/14 */

In un compilatore C ++ 98, dovrebbe verificarsi quanto segue :

  • new A - valore indeterminato ( Aè POD)
  • new A()- zero-inizializzazione
  • new B - costrutto predefinito ( B::mnon è inizializzato, Bnon è POD)
  • new B()- costrutto predefinito ( B::mnon è inizializzato)
  • new C - costrutto predefinito ( C::mè inizializzato a zero, Cnon è POD)
  • new C()- costrutto predefinito ( C::mè inizializzato a zero)
  • new D - costrutto predefinito ( D::mnon è inizializzato, Dnon è POD)
  • new D()- costrutto predefinito? ( D::mnon è inizializzato)

In un compilatore conforme a C ++ 03, le cose dovrebbero funzionare così:

  • new A - valore indeterminato ( Aè POD)
  • new A() - value-initialize A, che è zero-inizializzazione poiché è un POD.
  • new B - default-initializes (lascia B::mnon inizializzato, Bnon è POD)
  • new B() - value-initializes Bche azzera tutti i campi poiché il suo ctor predefinito è generato dal compilatore invece di quello definito dall'utente.
  • new C - default-initializes C, che chiama il ctor predefinito. ( C::mè inizializzato a zero, Cnon è POD)
  • new C() - value-initializes C, che chiama il ctor predefinito. ( C::mè zero-inizializzato)
  • new D - costrutto predefinito ( D::mnon è inizializzato, Dnon è POD)
  • new D() - valore-inizializza D? , che chiama il ctor predefinito ( D::mnon è inizializzato)

Valori italici e? sono incertezze, per favore aiutaci a correggere questo :-)

In un compilatore conforme a C ++ 11, le cose dovrebbero funzionare così:

??? (per favore aiutatemi se comincio da qui andrà comunque storto)

In un compilatore conforme a C ++ 14, le cose dovrebbero funzionare così: ??? (per favore aiutatemi se inizio da qui andrà comunque storto) (Bozza basata sulla risposta)

  • new A - inizializzazione di default A, compilatore gen. ctor, (leavs A::muninitialized) ( Ais POD)

  • new A() - value-initializes A, che è zero-inizializzazione dal 2. punto in [dcl.init] / 8

  • new B - inizializzazione di default B, compilatore gen. ctor, (leavs B::muninitialized) ( Bis non-POD)

  • new B() - value-initializes Bche azzera tutti i campi poiché il suo ctor predefinito è generato dal compilatore invece di quello definito dall'utente.

  • new C - default-initializes C, che chiama il ctor predefinito. ( C::mè inizializzato a zero, Cnon è POD)

  • new C() - value-initializes C, che chiama il ctor predefinito. ( C::mè zero-inizializzato)

  • new D - default-initializes D( D::mnon è inizializzato, Dnon è POD)

  • new D() - value-initializes D, che chiama il ctor predefinito ( D::mnon è inizializzato)

  • new E - default-initializes E, che chiama comp. gen. ctor. ( E::mnon è inizializzato, E non è POD)

  • new E() - value-initializes E, che inizializza a zero Edal 2 punto in [dcl.init] / 8 )

  • new F - default-initializes F, che chiama comp. gen. ctor. ( F::mnon è inizializzato, Fnon è POD)

  • new F() - value-initializes F, che viene inizializzato per impostazione predefinita F dal 1. punto in [dcl.init] / 8 (la Ffunzione ctor è fornita dall'utente se è dichiarata dall'utente e non è esplicitamente predefinita o eliminata nella sua prima dichiarazione. Link )



1
Per quanto ne so, c'è solo una differenza tra C ++ 98 e C ++ 03 in questi esempi. Il problema sembra essere descritto in N1161 (ci sono revisioni successive di quel documento) e CWG DR # 178 . La formulazione doveva cambiare in C ++ 11 a causa di nuove funzionalità e una nuova specifica di POD, ed è cambiata di nuovo in C ++ 14 a causa di difetti nella dicitura C ++ 11, ma gli effetti in questi casi non sono cambiati .
giorno

3
Anche se noioso, struct D { D() {}; int m; };può valere la pena includerlo nella tua lista.
Yakk - Adam Nevraumont

Risposte:


24

C ++ 14 specifica l'inizializzazione degli oggetti creati con new[expr.new] / 17 ([expr.new] / 15 in C ++ 11, e la nota non era una nota ma un testo normativo all'epoca):

Una nuova espressione che crea un oggetto di tipo Tinizializza quell'oggetto come segue:

  • Se il nuovo inizializzatore viene omesso, l'oggetto viene inizializzato per impostazione predefinita (8.5). [ Nota: se non viene eseguita alcuna inizializzazione, l'oggetto ha un valore indeterminato. - nota finale ]
  • Altrimenti, il nuovo inizializzatore viene interpretato in base alle regole di inizializzazione di 8.5 per l'inizializzazione diretta .

L'inizializzazione predefinita è definita in [dcl.init] / 7 (/ 6 in C ++ 11 e la formulazione stessa ha lo stesso effetto):

Inizializzare per impostazione predefinita un oggetto di tipo Tsignifica:

  • se Tè un tipo di classe (possibilmente qualificato da cv) (clausola 9), Tviene chiamato il costruttore predefinito (12.1) per (e l'inizializzazione è mal formata seT non ha un costruttore predefinito o la risoluzione dell'overload (13.3) risulta in un'ambiguità o in una funzione cancellata o inaccessibile dal contesto dell'inizializzazione);
  • se Tè un tipo di matrice, ogni elemento è inizializzato per impostazione predefinita ;
  • in caso contrario, non viene eseguita alcuna inizializzazione.

Così

  • new Afa solo in modo Ache venga chiamato il costruttore predefinito di s, che non viene inizializzato m. Valore indeterminato. Dovrebbe essere lo stesso per new B.
  • new A() è interpretato secondo [dcl.init] / 11 (/ 10 in C ++ 11):

    Un oggetto il cui inizializzatore è un insieme vuoto di parentesi, cioè (), deve essere inizializzato con valore.

    E ora considera [dcl.init] / 8 (/ 7 in C ++ 11 †):

    Per valore-inizializzare un oggetto di tipo Tmezzi:

    • se Tè un tipo di classe (possibilmente qualificato da cv) (clausola 9) senza alcun costruttore predefinito (12.1) o un costruttore predefinito fornito dall'utente o eliminato, l'oggetto è inizializzato per impostazione predefinita;
    • se Tè un tipo di classe (possibilmente qualificato da cv) senza un costruttore predefinito fornito dall'utente o cancellato, l'oggetto viene inizializzato zero e vengono verificati i vincoli semantici per l'inizializzazione predefinita e se T ha un costruttore predefinito non banale, l'oggetto è inizializzato di default;
    • se Tè un tipo array, ogni elemento è inizializzato con valore;
    • in caso contrario, l'oggetto viene inizializzato con zero.

    Quindi new A()verrà inizializzato a zero m. E questo dovrebbe essere equivalente a Ae B.

  • new Ce new C()inizializzerà nuovamente l'oggetto per impostazione predefinita, poiché si applica il primo punto elenco dall'ultima virgoletta (C ha un costruttore predefinito fornito dall'utente!). Ma, chiaramente, ora mè inizializzato nel costruttore in entrambi i casi.


† Bene, questo paragrafo ha una formulazione leggermente diversa in C ++ 11, che non altera il risultato:

Per valore-inizializzare un oggetto di tipo Tmezzi:

  • se Tè un tipo di classe (possibilmente qualificato da cv) (clausola 9) con un costruttore fornito dall'utente (12.1), T viene chiamato il costruttore predefinito per (e l'inizializzazione è mal formata se T non ha un costruttore predefinito accessibile);
  • se Tè un tipo di classe non union (possibilmente qualificato da cv) senza un costruttore fornito dall'utente, l'oggetto è inizializzato zero e, se il Tcostruttore predefinito dichiarato implicitamente non è banale, viene chiamato quel costruttore.
  • se Tè un tipo array, ogni elemento è inizializzato con valore;
  • in caso contrario, l'oggetto viene inizializzato con zero.

@ Gabriel non proprio.
Columbo

ah quindi stai parlando principalmente di c ++ 14 e i riferimenti per c ++ 11 sono dati tra parentesi
Gabriel

@ Gabriel Correct. Voglio dire, C ++ 14 è lo standard più recente, quindi è in primo piano.
Columbo

1
La cosa fastidiosa nel cercare di tracciare le regole di inizializzazione attraverso gli standard è che molte delle modifiche (quasi tutte?) Tra gli standard pubblicati C ++ 14 e C ++ 11 sono avvenute tramite DR, e così sono de facto C ++ 11 . E poi ci sono anche DR post-C ++ 14 ...
TC

@Columbo Ancora non capisco perché struct A { int m; }; struct C { C() : m(){}; int m; };producono risultati diversi e cosa causa l'inizializzazione di m in A in primo luogo. Ho aperto un thread dedicato per l'esperimento che ho fatto e apprezzerò il tuo contributo per chiarire il problema. Grazie stackoverflow.com/questions/45290121/...
darkThoughts

12

La risposta seguente estende la risposta https://stackoverflow.com/a/620402/977038 che servirebbe come riferimento per C ++ 98 e C ++ 03

Citando la risposta

  1. In C ++ 1998 ci sono 2 tipi di inizializzazione: zero e predefinita
  2. In C ++ 2003 è stato aggiunto un terzo tipo di inizializzazione, l'inizializzazione del valore.

C ++ 11 (in riferimento a n3242)

Inizializzatori

8.5 Inizializzatori [dcl.init] specifica che una variabile POD o non POD può essere inizializzata come parentesi graffa o uguale inizializzatore che può essere tra parentesi -init-list o inizializzatore-clausola aggregata come parentesi graffa-o-uguale- inizializzatore o utilizzando (elenco-espressioni) . Prima di C ++ 11, solo la clausola (elenco di espressioni) o inizializzatore era supportata sebbene la clausola inizializzatore fosse più limitata rispetto a quella che abbiamo in C ++ 11. In C ++ 11, la clausola di inizializzazione ora supporta l' elenco con parentesi graffe a parte all'espressione di assegnazionecome in C ++ 03. La seguente grammatica riassume la nuova clausola supportata, dove la parte è in grassetto è stata recentemente aggiunta nello standard C ++ 11.

inizializzatore:
    parentesi graffa-o-uguale-inizializzatore
    (elenco-espressioni)
parentesi-o-uguale-inizializzatore:
    = clausola-inizializzatore tra parentesi-
    inizializzazione-elenco
clausola-inizializzatore:
    assegnazione-espressione tra parentesi
    -elenco
-inizializzatore-elenco:
    inizializzatore-clausola ... opt
    initializer-list, initializer-clause ... opt **
braced-init-list:
    {initializer-list, opt}
    {}

Inizializzazione

Come C ++ 03, C ++ 11 supporta ancora tre forme di inizializzazione


Nota

La parte evidenziata in grassetto è stata aggiunta in C ++ 11 e quella barrata è stata rimossa da C ++ 11.

  1. Tipo di inizializzatore: 8.5.5 [dcl.init] _zero-initialize_

Eseguito nei seguenti casi

  • Gli oggetti con durata di archiviazione statica o thread sono inizializzati zero
  • Se ci sono meno inizializzatori rispetto agli elementi dell'array, ogni elemento non esplicitamente inizializzato deve essere inizializzato con zero
  • Durante l' inizializzazione del valore , se T è un tipo di classe non union (possibilmente qualificato da cv) senza un costruttore fornito dall'utente, l'oggetto viene inizializzato zero.

Inizializzare a zero un oggetto o un riferimento di tipo T significa:

  • se T è un tipo scalare (3.9), l'oggetto è posto al valore 0 (zero), preso come espressione di costante integrale , convertito in T;
  • se T è un tipo di classe non union (possibilmente qualificato cv) , ogni membro di dati non statici e ogni sottooggetto della classe base viene inizializzato da zero e il riempimento è inizializzato a zero bit;
  • se T è un tipo di unione (possibilmente qualificato da cv) , il primo membro di dati denominato non statico dell'oggetto viene inizializzato a zero e il riempimento viene inizializzato a zero bit;
  • se T è un tipo array, ogni elemento è inizializzato con zero;
  • se T è un tipo di riferimento, non viene eseguita alcuna inizializzazione.

2. Tipo di inizializzatore: 8.5.6 [dcl.init] _default-initialize_

Eseguito nei seguenti casi

  • Se il nuovo inizializzatore viene omesso, l'oggetto viene inizializzato per impostazione predefinita; se non viene eseguita alcuna inizializzazione, l'oggetto ha un valore indeterminato.
  • Se non viene specificato alcun inizializzatore per un oggetto, l'oggetto viene inizializzato per impostazione predefinita, ad eccezione degli oggetti con durata di archiviazione statica o thread
  • Quando una classe base o un membro dati non statico non è menzionato in un elenco di inizializzatori del costruttore e viene chiamato quel costruttore.

Inizializzare per impostazione predefinita un oggetto di tipo T significa:

  • se T è un tipo di classe non POD (possibilmente qualificato cv) (clausola 9), viene chiamato il costruttore predefinito per T (e l'inizializzazione è mal formata se T non ha un costruttore predefinito accessibile);
  • se T è un tipo di matrice, ogni elemento è inizializzato per impostazione predefinita;
  • in caso contrario, non viene eseguita alcuna inizializzazione.

Nota Fino a C ++ 11, solo i tipi di classe non POD con durata di archiviazione automatica erano considerati inizializzati per impostazione predefinita quando non veniva utilizzato alcun inizializzatore.


3. Tipo di inizializzatore: 8.5.7 [dcl.init] _value-initialize_

  1. Quando un oggetto (temporaneo senza nome, variabile denominata, durata della memorizzazione dinamica o membro dati non statico) il cui inizializzatore è un insieme vuoto di parentesi, ad esempio, () o parentesi graffe {}

Inizializzare un valore di un oggetto di tipo T significa:

  • se T è un tipo di classe (possibilmente qualificato da cv) (clausola 9) con un costruttore fornito dall'utente (12.1), viene chiamato il costruttore predefinito per T (e l'inizializzazione è mal formata se T non ha un costruttore predefinito accessibile) ;
  • se T è un tipo di classe non union (possibilmente qualificato cv) senza un costruttore fornito dall'utente, ogni membro di dati non statici e componente di classe base di T è inizializzato con valore; allora l'oggetto è inizializzato da zero e, se il costruttore predefinito dichiarato implicitamente di T non è banale, viene chiamato quel costruttore.
  • se T è un tipo di matrice, ogni elemento è inizializzato con valore;
  • in caso contrario, l'oggetto viene inizializzato con zero.

Quindi per riassumere

Nota La citazione pertinente dallo standard è evidenziata in grassetto

  • new A: default-initializes (lascia A :: m non inizializzato)
  • new A (): zero-inizializza A, poiché il valore inizializzato candidato non ha un costruttore predefinito fornito dall'utente o eliminato. se T è un tipo di classe non union (possibilmente qualificato da cv) senza un costruttore fornito dall'utente, l'oggetto è inizializzato zero e, se il costruttore predefinito dichiarato implicitamente di T non è banale, viene chiamato quel costruttore.
  • new B: default-initializes (lascia B :: m non inizializzato)
  • new B (): valore-inizializza B che azzera tutti i campi; se T è un tipo di classe (possibilmente qualificato da cv) (clausola 9) con un costruttore fornito dall'utente (12.1), allora viene chiamato il costruttore predefinito per T
  • new C: default-inizializza C, che chiama il ctor predefinito. se T è un tipo di classe (possibilmente qualificato da cv) (clausola 9), viene chiamato il costruttore predefinito per T , inoltre se il nuovo inizializzatore viene omesso, l'oggetto viene inizializzato per impostazione predefinita
  • new C (): value-inizializza C, che chiama il ctor predefinito. se T è un tipo di classe (possibilmente qualificato da cv) (clausola 9) con un costruttore fornito dall'utente (12.1), viene chiamato il costruttore predefinito per T. Inoltre, un oggetto il cui inizializzatore è un insieme vuoto di parentesi, cioè (), deve essere inizializzato con valore

0

Posso confermare che in C ++ 11, tutto ciò che è menzionato nella domanda in C ++ 14 è corretto, almeno secondo le implementazioni del compilatore.

Per verificarlo, ho aggiunto il seguente codice alla mia suite di test . Ho provato con -std=c++11 -O3GCC 7.4.0, GCC 5.4.0, Clang 10.0.1 e VS 2017 e tutti i test seguenti sono stati superati.

#include <gtest/gtest.h>
#include <memory>

struct A { int m;                    };
struct B { int m;            ~B(){}; };
struct C { int m; C():m(){}; ~C(){}; };
struct D { int m; D(){};             };
struct E { int m; E() = default;     };
struct F { int m; F();               }; F::F() = default;

// We use this macro to fill stack memory with something else than 0.
// Subsequent calls to EXPECT_NE(a.m, 0) are undefined behavior in theory, but
// pass in practice, and help illustrate that `a.m` is indeed not initialized
// to zero. Note that we initially tried the more aggressive test
// EXPECT_EQ(a.m, 42), but it didn't pass on all compilers (a.m wasn't equal to
// 42, but was still equal to some garbage value, not zero).
//
#define FILL { int m = 42; EXPECT_EQ(m, 42); }

// We use this macro to fill heap memory with something else than 0, before
// doing a placement new at that same exact location. Subsequent calls to
// EXPECT_EQ(a->m, 42) are undefined behavior in theory, but pass in practice,
// and help illustrate that `a->m` is indeed not initialized to zero.
//
#define FILLH(b) std::unique_ptr<int> bp(new int(42)); int* b = bp.get(); EXPECT_EQ(*b, 42)

TEST(TestZero, StackDefaultInitialization)
{
    { FILL; A a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; B a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; C a; EXPECT_EQ(a.m, 0); }
    { FILL; D a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; F a; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackValueInitialization)
{
    { FILL; A a = A(); EXPECT_EQ(a.m, 0); }
    { FILL; B a = B(); EXPECT_EQ(a.m, 0); }
    { FILL; C a = C(); EXPECT_EQ(a.m, 0); }
    { FILL; D a = D(); EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a = E(); EXPECT_EQ(a.m, 0); }
    { FILL; F a = F(); EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackListInitialization)
{
    { FILL; A a{}; EXPECT_EQ(a.m, 0); }
    { FILL; B a{}; EXPECT_EQ(a.m, 0); }
    { FILL; C a{}; EXPECT_EQ(a.m, 0); }
    { FILL; D a{}; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a{}; EXPECT_EQ(a.m, 0); }
    { FILL; F a{}; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, HeapDefaultInitialization)
{
    { FILLH(b); A* a = new (b) A; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); B* a = new (b) B; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); C* a = new (b) C; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); F* a = new (b) F; EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapValueInitialization)
{
    { FILLH(b); A* a = new (b) A(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D(); EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F(); EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapListInitialization)
{
    { FILLH(b); A* a = new (b) A{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D{}; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F{}; EXPECT_EQ(a->m, 42); } // ~UB
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

I punti in cui UB!viene menzionato sono comportamenti indefiniti e il comportamento effettivo dipende probabilmente da molti fattori ( a.mpotrebbe essere uguale a 42, 0 o qualche altra spazzatura). I luoghi in cui ~UBviene menzionato sono anche comportamenti indefiniti in teoria, ma in pratica, a causa dell'utilizzo di un posizionamento nuovo, è molto improbabile che a->msarà uguale a qualsiasi altra cosa rispetto a 42.

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.