Confusão de inicialização padrão, valor e zero

89

Estou muito confuso sobre a inicialização de valor e padrão e zero. e especialmente quando eles são iniciados para os diferentes padrões C ++ 03 e C ++ 11 (e C ++ 14 ).

Estou citando e tentando estender uma resposta realmente boa Valor- / Padrão- / Zero- Init C ++ 98 e C ++ 03 aqui para torná-lo mais geral, pois ajudaria muitos usuários se alguém pudesse ajudar a preencher o lacunas necessárias para ter uma boa visão geral sobre o que acontece quando?

O insight completo por exemplos em poucas palavras:

Às vezes, a memória retornada pelo novo operador será inicializada, e às vezes não, dependendo se o tipo que você está atualizando é um POD (dados antigos simples) , ou se é uma classe que contém membros POD e está usando um construtor padrão gerado pelo compilador.

  • Em C ++ 1998 existem 2 tipos de inicialização: zero e default-inicialização
  • Em C ++ 2003, um terceiro tipo de inicialização, inicialização de valor foi adicionado.
  • Em C ++ 2011 / C ++ 2014, apenas a inicialização de lista foi adicionada e as regras para inicialização de valor / padrão / zero mudaram um pouco.

Presumir:

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 */

Em um compilador C ++ 98, o seguinte deve ocorrer :

  • new A - valor indeterminado ( Aé POD)
  • new A()- inicializar zero
  • new B - construção padrão ( B::mnão inicializada, Bnão POD)
  • new B()- construção padrão ( B::mnão inicializada)
  • new C - construção padrão ( C::mé inicializado com zero, Cnão é POD)
  • new C()- construção padrão ( C::mé inicializado com zero)
  • new D - construção padrão ( D::mnão inicializada, Dnão POD)
  • new D()- construção padrão? ( D::mnão foi inicializado)

Em um compilador compatível com C ++ 03, as coisas devem funcionar assim:

  • new A - valor indeterminado ( Aé POD)
  • new A() - value-initialize A, que é inicialização zero, pois é um POD.
  • new B - inicializa por padrão (deixa B::mnão inicializado, Bnão é POD)
  • new B() - value-initializes Bque inicializa com zero todos os campos, pois seu ctor padrão é gerado pelo compilador em oposição ao definido pelo usuário.
  • new C - default-initializes C, que chama o ctor padrão. ( C::mé inicializado com zero, Cnão é POD)
  • new C() - value-initializes C, que chama o ctor padrão. ( C::mé inicializado com zero)
  • new D - construção padrão ( D::mnão inicializada, Dnão POD)
  • new D() - o valor inicializa D? , que chama o ctor padrão ( D::mnão foi inicializado)

Valores em itálico e? são incertezas, por favor ajude a corrigir isso :-)

Em um compilador compatível com C ++ 11, as coisas devem funcionar assim:

??? (por favor me ajude se eu começar aqui, mesmo assim, vai dar errado)

Em um compilador compatível com C ++ 14, as coisas devem funcionar assim: ??? (por favor, ajude se eu começar aqui, de qualquer forma, ele dará errado) (Rascunho com base na resposta)

  • new A - inicializa por padrão A, compilador gen. ctor, (deixa de ser A::minicializado) ( Aé POD)

  • new A() - inicializa o valor A, que é a inicialização de zero desde 2. ponto em [dcl.init] / 8

  • new B - inicializa por padrão B, compilador gen. ctor, (deixa de ser B::minicializado) ( Bnão é POD)

  • new B() - value-initializes Bque inicializa com zero todos os campos, pois seu ctor padrão é gerado pelo compilador em oposição ao definido pelo usuário.

  • new C - default-initializes C, que chama o ctor padrão. ( C::mé inicializado com zero, Cnão é POD)

  • new C() - value-initializes C, que chama o ctor padrão. ( C::mé inicializado com zero)

  • new D - inicializa por padrão D( D::mnão é inicializado, Dnão é POD)

  • new D() - value-initializes D, que chama o ctor padrão ( D::mnão é inicializado)

  • new E - default-initializes E, que chama o comp. gen. ctor. ( E::mnão foi inicializado, E não é POD)

  • new E() - inicializa o valor E, que é inicializado com zero Edesde 2 pontos em [dcl.init] / 8 )

  • new F - default-initializes F, que chama o comp. gen. ctor. ( F::mnão foi inicializado, Fnão é POD)

  • new F() - value-initializes F, que inicializa por padrão F desde 1. ponto em [dcl.init] / 8 (a Ffunção ctor é fornecida pelo usuário se for declarada pelo usuário e não explicitamente padronizada ou excluída em sua primeira declaração. Link )

Gabriel
fonte
há uma boa explicação disso aqui: en.cppreference.com/w/cpp/language/default_constructor
Richard Hodges,
1
Pelo que eu posso dizer, há apenas uma diferença entre C ++ 98 e C ++ 03 nesses exemplos. O problema parece estar descrito no N1161 (há revisões posteriores desse documento) e no CWG DR # 178 . O texto precisou ser alterado no C ++ 11 devido a novos recursos e uma nova especificação do POD, e foi alterado novamente no C ++ 14 devido a defeitos no texto do C ++ 11, mas os efeitos nestes casos não são alterados .
dyp em
3
Embora enfadonho, struct D { D() {}; int m; };pode valer a pena incluí-lo em sua lista.
Yakk - Adam Nevraumont

Respostas:

24

C ++ 14 especifica a inicialização de objetos criados com newem [expr.new] / 17 ([expr.new] / 15 em C ++ 11, e a nota não era uma nota, mas um texto normativo naquela época):

Uma nova expressão que cria um objeto do tipo Tinicializa esse objeto da seguinte maneira:

  • Se o new-initializer for omitido, o objeto será inicializado por padrão (8.5). [ Nota: Se nenhuma inicialização for realizada, o objeto tem um valor indeterminado. - nota final ]
  • Caso contrário, o novo inicializador é interpretado de acordo com as regras de inicialização de 8.5 para inicialização direta .

A inicialização padrão é definida em [dcl.init] / 7 (/ 6 em C ++ 11, e o próprio texto tem o mesmo efeito):

Para padrão-inicializar um objeto do tipo Tmeios:

  • se Tfor um tipo de classe (possivelmente cv-qualificado) (Cláusula 9), o construtor padrão (12.1) para Té chamado (e a inicialização é malformada se Tnão tiver um construtor padrão ou resolução de sobrecarga (13.3) resulta em uma ambiguidade ou em uma função que foi excluída ou inacessível do contexto da inicialização);
  • se Tfor um tipo de array, cada elemento será inicializado por padrão ;
  • caso contrário, nenhuma inicialização é executada.

portanto

  • new Aapenas faz com que Ao construtor padrão seja chamado, o que não inicializa m. Valor indeterminado. Deve ser o mesmo para new B.
  • new A() é interpretado de acordo com [dcl.init] / 11 (/ 10 em C ++ 11):

    Um objeto cujo inicializador é um conjunto vazio de parênteses, ou seja (), deve ser inicializado com valor.

    E agora considere [dcl.init] / 8 (/ 7 em C ++ 11 †):

    Para valor-inicializar um objecto do tipo Tmeios:

    • se Tfor um tipo de classe (possivelmente qualificado por cv) (Cláusula 9) sem nenhum construtor padrão (12.1) ou um construtor padrão fornecido ou excluído pelo usuário, então o objeto é inicializado por padrão;
    • se Tfor um tipo de classe (possivelmente qualificado pelo cv) sem um construtor padrão fornecido pelo usuário ou excluído, então o objeto é inicializado com zero e as restrições semânticas para a inicialização padrão são verificadas, e se T tem um construtor padrão não trivial, o objeto é inicializado por padrão;
    • se Tfor um tipo de array, cada elemento será inicializado com valor;
    • caso contrário, o objeto é inicializado com zero.

    Portanto, new A()será inicializado em zero m. E isso deve ser equivalente para Ae B.

  • new Ce new C()inicializará o objeto por padrão novamente, já que o primeiro ponto da última citação se aplica (C tem um construtor padrão fornecido pelo usuário!). Mas, claramente, now mé inicializado no construtor em ambos os casos.


† Bem, este parágrafo tem uma redação ligeiramente diferente em C ++ 11, o que não altera o resultado:

Para valor-inicializar um objecto do tipo Tmeios:

  • se Tfor um tipo de classe (possivelmente qualificado por cv) (Cláusula 9) com um construtor fornecido pelo usuário (12.1), então o construtor padrão para T é chamado (e a inicialização é mal formada se T não tiver um construtor padrão acessível);
  • if Tfor um tipo de classe não-união (possivelmente qualificado por cv) sem um construtor fornecido pelo usuário, então o objeto é inicializado com zero e, se To construtor padrão declarado implicitamente não for trivial, esse construtor é chamado.
  • se Tfor um tipo de array, cada elemento será inicializado com valor;
  • caso contrário, o objeto é inicializado com zero.
Columbo
fonte
ah, então você está falando principalmente sobre c ++ 14 e as referências para c ++ 11 são fornecidas entre colchetes
Gabriel,
@Gabriel Correct. Quer dizer, C ++ 14 é o padrão mais recente, então isso está em destaque.
Columbo
1
O chato de tentar rastrear as regras de inicialização entre os padrões é que muitas das mudanças (a maioria? Todas?) Entre os padrões publicados C ++ 14 e C ++ 11 aconteceram por meio de DRs, assim como o C ++ 11 de fato . E também há DRs pós-C ++ 14 ...
TC
@Columbo Eu ainda não entendo por que struct A { int m; }; struct C { C() : m(){}; int m; };produzir resultados diferentes e o que faz com que m em A seja inicializado em primeiro lugar. Abri um tópico dedicado ao experimento que fiz e agradecerei sua contribuição para esclarecer o problema. Obrigado stackoverflow.com/questions/45290121/…
darkThoughts
12

A resposta a seguir estende a resposta https://stackoverflow.com/a/620402/977038 que serviria como uma referência para C ++ 98 e C ++ 03

Citando a resposta

  1. Em C ++ 1998, existem 2 tipos de inicialização: zero e padrão
  2. Em C ++ 2003, um terceiro tipo de inicialização, inicialização de valor foi adicionado.

C ++ 11 (em referência a n3242)

Inicializadores

8,5 Initializers [dcl.init] determina que uma variável POD ou não POD pode ser inicializado quer como cinta-ou-igual inicializador que pode ser preparou-init-lista ou inicializador-cláusula no agregado, referido como cinta-ou-igual- inicializador ou usando (lista de expressão) . Antes do C ++ 11, apenas (lista de expressões) ou cláusula inicializadora era suportada, embora a cláusula inicializadora fosse mais restrita do que temos no C ++ 11. No C ++ 11, a cláusula de inicialização agora oferece suporte a lista de inicialização com chaves além da expressão de atribuiçãocomo no C ++ 03. A gramática a seguir resume a nova cláusula suportada, em que a parte em negrito foi adicionada recentemente ao padrão C ++ 11.

inicializador:
    chave-ou-igual-inicializador
    (lista de expressões)
chave-ou-igual-inicializador:
    = cláusula
    -inicializável-lista
-inicialização-cláusula-inicial:
    expressão-atribuição
    -lista-inicial-chave-
inicializador-lista:
    cláusula-inicializador ... opt
    initializer-list, initializer-clause ... opt **
braced-init-list:
    {initializer-list, opt}
    {}

Inicialização

Como C ++ 03, C ++ 11 ainda suporta três formas de inicialização


Nota

A parte destacada em negrito foi adicionada em C ++ 11 e a parte destacada foi removida do C ++ 11.

  1. Tipo de inicializador: 8.5.5 [dcl.init] _zero-initialize_

Executado nos seguintes casos

  • Objetos com duração de armazenamento estático ou thread são inicializados com zero
  • Se houver menos inicializadores do que elementos do array, cada elemento não explicitamente inicializado será inicializado com zero
  • Durante a inicialização do valor , se T for um tipo de classe não-união (possivelmente qualificado por cv) sem um construtor fornecido pelo usuário, então o objeto é inicializado com zero.

Inicializar a zero um objeto ou referência do tipo T significa:

  • se T for do tipo escalar (3.9), o objeto é definido com o valor 0 (zero), tomado como uma expressão constante integral , convertido em T;
  • se T for um tipo de classe não-união (possivelmente qualificado pelo cv) , cada membro de dados não estático e cada subobjeto da classe base é inicializado com zero e o preenchimento é inicializado com bits zero;
  • se T for um tipo de união (possivelmente qualificado por cv) , o primeiro membro de dados nomeado não estático do objeto é inicializado com zero e o preenchimento é inicializado com bits zero;
  • se T é um tipo de array, cada elemento é inicializado com zero;
  • se T for um tipo de referência, nenhuma inicialização é executada.

2. Tipo de inicializador: 8.5.6 [dcl.init] _default-initialize_

Executado nos seguintes casos

  • Se o new-initializer for omitido, o objeto será inicializado por padrão; se nenhuma inicialização for realizada, o objeto tem valor indeterminado.
  • Se nenhum inicializador for especificado para um objeto, o objeto é inicializado por padrão, exceto para Objetos com duração de armazenamento estático ou thread
  • Quando uma classe base ou um membro de dados não estático não é mencionado em uma lista de inicializadores de construtor e esse construtor é chamado.

Inicializar por padrão um objeto do tipo T significa:

  • se T for um tipo de classe não POD (possivelmente qualificado pelo cv) (Cláusula 9), o construtor padrão para T é chamado (e a inicialização é mal formada se T não tiver um construtor padrão acessível);
  • se T for um tipo de array, cada elemento é inicializado por padrão;
  • caso contrário, nenhuma inicialização é executada.

Nota Até o C ++ 11, apenas os tipos de classe não POD com duração de armazenamento automático eram considerados inicializados por padrão quando nenhum inicializador era usado.


3. Tipo de inicializador: 8.5.7 [dcl.init] _value-initialize_

  1. Quando um objeto (temporário sem nome, variável nomeada, duração de armazenamento dinâmico ou membro de dados não estáticos) cujo inicializador é um conjunto vazio de parênteses, ou seja, () ou colchetes {}

Inicializar o valor de um objeto do tipo T significa:

  • se T for um tipo de classe (possivelmente qualificado por cv) (Cláusula 9) com um construtor fornecido pelo usuário (12.1), então o construtor padrão para T é chamado (e a inicialização é malformada se T não tiver nenhum construtor padrão acessível) ;
  • se T for um tipo de classe não-união (possivelmente qualificado pelo cv) sem um construtor fornecido pelo usuário, então cada membro de dados não estático e componente da classe base de T é inicializado por valor; então o objeto é inicializado com zero e, se o construtor padrão implicitamente declarado de T não for trivial, esse construtor é chamado.
  • se T for um tipo de array, então cada elemento é inicializado com valor;
  • caso contrário, o objeto é inicializado com zero.

Então, para resumir

Nota A citação relevante da norma é destacada em negrito

  • novo A: inicializa por padrão (deixa A :: m não inicializado)
  • new A (): Zero-initialize A, visto que o valor inicializado candidato não tem um construtor padrão fornecido pelo usuário ou excluído. se T for um tipo de classe não-união (possivelmente qualificado pelo cv) sem um construtor fornecido pelo usuário, então o objeto é inicializado com zero e, se o construtor padrão implicitamente declarado de T não for trivial, esse construtor é chamado.
  • novo B: inicializa por padrão (deixa B :: m não inicializado)
  • new B (): inicializa o valor B que inicializa com zero todos os campos; se T for um tipo de classe (possivelmente qualificado por cv) (Cláusula 9) com um construtor fornecido pelo usuário (12.1), então o construtor padrão para T é chamado
  • novo C: inicializa o C por padrão, que chama o ctor padrão. se T for um tipo de classe (possivelmente qualificado por cv) (Cláusula 9), o construtor padrão para T é chamado , Além disso, se o novo inicializador for omitido, o objeto será inicializado por padrão
  • new C (): valor-inicializa C, que chama o ctor padrão. se T for um tipo de classe (possivelmente qualificado para cv) (Cláusula 9) com um construtor fornecido pelo usuário (12.1), então o construtor padrão para T é chamado. Além disso, um objeto cujo inicializador é um conjunto vazio de parênteses, ou seja, (), deve ser inicializado com valor
Abhijit
fonte
0

Posso confirmar que em C ++ 11, tudo mencionado na pergunta em C ++ 14 está correto, pelo menos de acordo com as implementações do compilador.

Para verificar isso, adicionei o seguinte código ao meu conjunto de testes . Eu testei -std=c++11 -O3no GCC 7.4.0, GCC 5.4.0, Clang 10.0.1 e VS 2017, e todos os testes abaixo foram aprovados.

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

Os locais onde UB!é mencionado são comportamentos indefinidos e o comportamento real provavelmente depende de muitos fatores ( a.mpode ser igual a 42, 0 ou algum outro lixo). Os locais onde ~UBé mencionado também são comportamentos indefinidos em teoria, mas na prática, devido ao uso de um posicionamento novo, é muito improvável que a->mseja igual a qualquer coisa diferente de 42.

Boris Dalstein
fonte