Inicialização zero em C ++ - Por que `b` neste programa não foi inicializado, mas` a` é inicializado?

135

De acordo com a resposta aceita (e apenas) para esta pergunta de estouro de pilha ,

Definindo o construtor com

MyTest() = default;

em vez disso, inicializará o objeto com zero.

Então, por que o seguinte,

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

produza esta saída:

0 32766

Ambos os construtores definidos são padrão? Certo? E para os tipos de POD, a inicialização padrão é a inicialização zero.

E de acordo com a resposta aceita para esta pergunta ,

  1. Se um membro do POD não for inicializado no construtor nem via inicialização em classe C ++ 11, ele será inicializado por padrão.

  2. A resposta é a mesma, independentemente da pilha ou pilha.

  3. No C ++ 98 (e não posteriormente), o novo int () foi especificado como executando a inicialização zero.

Apesar de tentar envolver minha cabeça (ainda que minúscula ) nos construtores padrão e na inicialização padrão , não consegui encontrar uma explicação.

Duck Dodgers
fonte
3
Curiosamente, até recebo um aviso para b: main.cpp: 18: 34: warning: 'b.bar::b' é usado não inicializado nesta função [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl
8
barO construtor é fornecido foopelo usuário, enquanto o construtor é o padrão.
Jarod42
2
@PeteBecker, eu entendo isso. Como eu poderia, de alguma forma, sacudir um pouco minha RAM para que, se houvesse zero lá, agora fosse outra coisa. ;) ps Eu executei o programa uma dúzia de vezes. Não é um grande programa. Você pode executá-lo e testá-lo em seu sistema. aé zero. bnão é. Parece aser inicializado.
Duck Dodgers
2
@JoeyMallone Em relação a "como é fornecida pelo usuário": Não há garantia de que a definição de bar::bar()seja visível main()- ela pode ser definida em uma unidade de compilação separada e fazer algo muito trivial, enquanto main()apenas a declaração é visível. Acho que você concorda que esse comportamento não deve mudar, dependendo se você coloca bar::bar()a definição em uma unidade de compilação separada ou não (mesmo que toda a situação não seja intuitiva).
Max Langhof
2
@balki Ou int a = 0;você quer ser realmente explícito.
precisa saber é o seguinte

Respostas:

109

A questão aqui é bastante sutil. Você pensaria que

bar::bar() = default;

daria a você um construtor padrão gerado pelo compilador, mas ele agora é considerado fornecido pelo usuário. [dcl.fct.def.default] / 5 estados:

As funções explicitamente padronizadas e as funções declaradas implicitamente são denominadas coletivamente funções padrão, e a implementação deve fornecer definições implícitas para elas ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign ]), o que pode significar defini-los como excluídos. Uma função é fornecida pelo usuário se for declarada pelo usuário e não for explicitamente padronizada ou excluída em sua primeira declaração.Uma função explicitamente padronizada fornecida pelo usuário (ou seja, explicitamente padronizada após sua primeira declaração) é definida no ponto em que é explicitamente padronizada; se tal função estiver implicitamente definida como excluída, o programa será mal formado. [Nota: Declarar uma função como padrão após sua primeira declaração pode fornecer execução eficiente e definição concisa enquanto permite uma interface binária estável para uma base de código em evolução. - nota final]

ênfase minha

Portanto, podemos ver que, como você não assumiu o padrão bar()quando o declarou pela primeira vez, agora é considerado fornecido pelo usuário. Por causa disso [dcl.init] /8.2

se T for um tipo de classe (possivelmente qualificado para cv) sem um construtor padrão excluído ou fornecido pelo usuário, o objeto será inicializado com zero e as restrições semânticas para a inicialização padrão serão verificadas e se T tiver um construtor padrão não trivial , o objeto é inicializado por padrão;

não se aplica mais e não estamos inicializando o valor, bmas inicializando o padrão por [dcl.init] /8.1

se T for um tipo de classe (possivelmente qualificado para cv) ([class]) sem construtor padrão ([class.default.ctor]) ou construtor padrão fornecido ou excluído pelo usuário, o objeto será inicializado por padrão ;

NathanOliver
fonte
52
Quero dizer (*_*)... Se, mesmo para usar as construções básicas da linguagem, preciso ler as letras miúdas do rascunho da linguagem, então Aleluia! Mas provavelmente parece ser o que você diz.
Duck Dodgers
12
@balki Sim, fazer bar::bar() = defaultfora de linha é o mesmo que fazer bar::bar(){}inline.
precisa saber é o seguinte
15
@ JoeyMallone Sim, C ++ pode ser bastante complicado. Não sei ao certo qual é a razão disso.
precisa saber é o seguinte
3
Se houver uma declaração anterior, uma definição subsequente com a palavra-chave padrão NÃO inicializará zero os membros. Certo? Isto está certo. É o que está acontecendo aqui.
precisa saber é o seguinte
6
O motivo está aí em sua citação: o objetivo de um padrão fora de linha é "fornecer execução eficiente e definição concisa, permitindo uma interface binária estável a uma base de código em evolução", em outras palavras, permitir que você alterne para um corpo escrito pelo usuário posteriormente, se necessário, sem interromper a ABI. Observe que a definição fora de linha não está implicitamente embutida e, portanto, só pode aparecer em uma TU por padrão; outro TU vendo a definição de classe sozinha não tem como saber se está definido explicitamente como padrão.
TC
25

A diferença de comportamento vem do fato de que, de acordo com [dcl.fct.def.default]/5, bar::baré fornecido pelo usuário onde foo::foonão é 1 . Como consequência, foo::fooirá valorizar-inicializar seus membros (ou seja: de zero-inicializar foo::a ), mas bar::barpermanecerá não inicializado 2 .


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

Uma função é fornecida pelo usuário se for declarada pelo usuário e não for explicitamente padronizada ou excluída em sua primeira declaração.

2)

De [dcl.init # 6] :

Valorizar a inicialização de um objeto do tipo T significa:

  • se T é um tipo de classe (possivelmente qualificado para cv) sem construtor padrão ([class.ctor]) ou construtor padrão fornecido ou excluído pelo usuário, o objeto é inicializado por padrão;

  • se T for um tipo de classe (possivelmente qualificado para cv) sem um construtor padrão excluído ou fornecido pelo usuário, o objeto será inicializado com zero e as restrições semânticas para a inicialização padrão serão verificadas e se T tiver um construtor padrão não trivial , o objeto é inicializado por padrão;

  • ...

Em [dcl.init.list] :

A inicialização da lista de um objeto ou referência do tipo T é definida da seguinte maneira:

  • ...

  • Caso contrário, se a lista do inicializador não tiver elementos e T for um tipo de classe com um construtor padrão, o objeto será inicializado por valor.

Da resposta de Vittorio Romeo

YSC
fonte
10

Da cppreference :

Inicialização agregada inicializa agregados. É uma forma de inicialização de lista.

Um agregado é um dos seguintes tipos:

[recorte]

  • tipo de classe [snip], que possui

    • [snip] (existem variações para diferentes versões padrão)

    • nenhum construtor fornecido, herdado ou explícito fornecido pelo usuário (são permitidos construtores explicitamente padronizados ou excluídos)

    • [snip] (existem mais regras que se aplicam a ambas as classes)

Dada essa definição, fooé um agregado, enquanto barnão é (possui construtor fornecido pelo usuário e não padronizado).

Portanto foo, para , T object {arg1, arg2, ...};é uma sintaxe para inicialização agregada.

Os efeitos da inicialização agregada são:

  • [snip] (alguns detalhes irrelevantes para este caso)

  • Se o número de cláusulas do inicializador for menor que o número de membros ou a lista do inicializador estiver completamente vazia, os membros restantes serão inicializados por valor .

Portanto, o a.avalor é inicializado, o que para intsignifica inicialização zero.

Pois bar, T object {};por outro lado, é a inicialização de valor (da instância da classe, não a inicialização de valor de membros!). Como é um tipo de classe com um construtor padrão, o construtor padrão é chamado. O construtor padrão que você definiu como padrão inicializa os membros (em virtude de não ter inicializadores de membros) que, no caso de int(com armazenamento não estático), saem b.bcom um valor indeterminado.

E para os tipos de pod, a inicialização padrão é a inicialização zero.

Não. Isso está errado.


PS Uma palavra sobre seu experimento e sua conclusão: ver que o resultado é zero não significa necessariamente que a variável foi inicializada com zero. Zero é o número perfeitamente possível para um valor de lixo.

por isso, executei o programa talvez 5 a 6 vezes antes de postar e cerca de 10 vezes agora, a é sempre zero. b muda um pouco.

O fato de o valor ter sido o mesmo várias vezes também não significa necessariamente que ele foi inicializado.

Eu também tentei com o conjunto (CMAKE_CXX_STANDARD 14). O resultado foi o mesmo.

O fato desse resultado ser o mesmo com várias opções do compilador não significa que a variável seja inicializada. (Embora em alguns casos, a alteração da versão padrão possa alterar se ela foi inicializada).

Como eu poderia, de alguma forma, sacudir um pouco minha RAM para que, se houvesse zero lá, agora fosse outra coisa

Não há maneira garantida no C ++ para fazer com que o valor do valor não inicializado apareça diferente de zero.

A única maneira de saber que uma variável é inicializada é comparar o programa com as regras da linguagem e verificar se as regras dizem que ela foi inicializada. Nesse caso, a.aé realmente inicializado.

eerorika
fonte
"O construtor padrão que você definiu como padrão inicializa os membros (em virtude de não ter inicializadores de membros), o que, no caso de int, o deixa com um valor indeterminado." -> eh! "para tipos de pod, a inicialização padrão é inicialização zero." ou eu estou errado?
Duck Dodgers
2
@JoeyMallone A inicialização padrão dos tipos de POD é sem inicialização.
precisa saber é o seguinte
@ NathanOliver, então estou ainda mais confuso. Então como aé inicializado. Eu estava pensando que aé inicializado por padrão e a inicialização padrão para um membro POD é, inicialização zero. É aentão apenas felizmente sempre chegando zero, não importa quantas vezes eu executar este programa.
Duck Dodgers
@ JoeyMallone Then how come a is initialized.Porque é um valor inicializado. I was thinking a is default initializedNão é.
eerorika
3
@JoeyMallone Não se preocupe. Você pode criar um livro com a inicialização em C ++. Se você receber uma oportunidade CppCon no youtube tem alguns vídeos na inicialização com o mais decepcionante (como em apontar como é ruim) sendo youtube.com/watch?v=7DTlWPgX6zs
NathanOliver
0

Tentei executar o snippet que você forneceu como test.cpp, por meio de gcc & clang e vários níveis de otimização:

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 ]

Então é aí que fica interessante, mostra claramente que o clang O0 build está lendo números aleatórios, presumivelmente espaço na pilha.

Eu rapidamente virei minha IDA para ver o que está acontecendo:

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

Agora, o que bar::bar(bar *this)faz?

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

Hmm, nada. Tivemos que recorrer ao 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

Então sim, é apenas, nada, o que o construtor basicamente faz é this = this. Mas sabemos que ele está realmente carregando endereços de pilha não inicializados aleatórios e imprimindo-os.

E se fornecermos explicitamente valores para as duas estruturas?

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

Acertar 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 semelhante com o 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 ]

Portanto, isso significa que é efetivamente uma inicialização direta bar b(0), não agregada.

Provavelmente, porque se você não fornecer uma implementação explícita do construtor, isso poderá ser um símbolo externo, por exemplo:

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

O compilador não é inteligente o suficiente para deduzir isso como uma chamada in-op / in-line em um estágio não otimizado.

Steve Fan
fonte