- A inicialização de objetos em C++ pode se dividir em default/value/list/aggregate initialization mesmo com sintaxes parecidas como
T t;, T t{};, T t{...}, e o estado real dos membros pode variar
- Dependendo de onde o construtor padrão é declarado com
= default, a ocorrência de zero-initialization muda; se ele for definido fora da classe, os membros podem permanecer com valores não inicializados
- Mesmo tipos que parecem ter o construtor padrão deletado por conterem um membro
const int podem, se forem aggregate, inicializar A a{}; por aggregate initialization e acabar com o valor 0
- A aggregate initialization baseada em parênteses do C++20 é mais permissiva que a inicialização com chaves, podendo causar diferenças como narrowing conversion e dangling reference
- Na prática, em vez de depender da combinação de construtores implícitos e regras de inicialização, é mais fácil evitar exceções deixando clara a intenção de inicialização com um construtor explícito
Onde a sintaxe de inicialização do C++ se divide
T t; executa default-initialization
- Se
T for um tipo de classe e tiver um construtor padrão, esse construtor é executado
- Se
T for um array, cada elemento é default-initialized
- Nos demais casos, nada é feito
T t{}; parece value-initialization, mas, por causa da sintaxe com chaves, primeiro é tratada como list-initialization
- Mesmo em exemplos simples, a diferença aparece imediatamente
int x{}; é value-initialized e inicializado como 0
int y; é default-initialized e seu valor não é inicializado
Pair p; e Pair q{};, com construtor padrão, chamam o construtor
SimplePair r;, que só tem membros, é default-initialized, e seus membros podem não ser inicializados
O resultado muda conforme onde o construtor padrão é declarado
- Se o usuário não declarar um construtor, o compilador declara um construtor padrão implicitly-declared
- Usar
T() = default; na primeira declaração dentro da classe é tratado pelo padrão de forma quase igual a um construtor declarado implicitamente
- Se um construtor padrão declarado implicitamente ou explicitamente defaulted não for deletado, o compilador fornece um implicitly-defined default constructor
- Na prática, ele deve ser equivalente a
T() {}, com corpo vazio e lista de inicialização de membros vazia
- No código a seguir,
t.x se torna 0
struct T {
int x;
T() = default;
};
T t{};
std::cout << t.x << std::endl;
- Isso acontece porque
t é value-initialized e, como o construtor padrão de T não é user-provided, primeiro ocorre zero-initialization e depois o construtor padrão é chamado
= default fora da classe e construtor user-provided
- Mesmo com o mesmo
= default, se ele for definido fora da classe, torna-se um construtor user-provided
struct T {
int x;
T();
};
T::T() = default;
T t{};
std::cout << t.x << std::endl;
- Nesse caso,
T::T() = default; não foi defaulted na primeira declaração, mas é um construtor definido como defaulted fora da classe
- As regras de value-initialization executam default-initialization quando há um construtor padrão user-provided ou deleted
- Portanto, no exemplo acima, apenas o construtor padrão é executado sem zero-initialization; como esse construtor não faz nada,
t.x fica com um valor lixo
Casos em que o construtor padrão é deletado
- Quando o compilador não consegue criar um construtor padrão razoável, o construtor padrão declarado implicitamente pode ser definido como deleted
- Condições representativas incluem:
- Há um membro de referência não estático
- Há um membro não estático ou uma classe-base não abstrata que não pode ser default-constructed ou destruída adequadamente
- Há um membro
const não estático sem inicializador padrão de membro, e esse membro não é const-default-constructible
- Se o usuário fornecer diretamente um construtor, o compilador não tenta definir um construtor padrão implícito e também não cria um construtor deleted
- Mesmo com essas condições, as regras de inicialização do C++ se comportam de modo geral de forma muito permissiva
O momento em que a aggregate initialization entra em cena
struct A { const int x; }; A a{}; parece que não compilaria porque o construtor padrão seria deletado, mas na prática compila e a.x se torna 0
- O ponto principal é que
A é um aggregate
- Um aggregate é um array ou uma classe que satisfaz as seguintes condições:
- Não tem construtores user-declared nem herdados
- Não tem membros de dados não estáticos diretos
private/protected
- Não tem classes-base diretas
private/protected
- Não tem funções virtuais nem classes-base virtuais
- Quando list-initialization é aplicada a um aggregate, salvo exceções específicas, aplica-se aggregate initialization
- Cada elemento da lista de inicialização inicializa, em ordem, cada elemento do aggregate
- Se o número de elementos da lista for insuficiente, os elementos restantes são inicializados por seus inicializadores padrão de membro, se existirem
- Se não houver inicializador padrão de membro e não for uma referência, o elemento é copy-initialized com uma lista de inicialização vazia
- Portanto,
A a{}; não é uma chamada de construtor, mas um caso em que a.x é copy-list-initialized com uma lista vazia e passa por value-initialization até chegar a zero-initialization
Regras adicionais da inicialização com chaves
- Sintaxes baseadas em chaves, como
T t{...} e T t = {...}, em geral são list-initialization
T t{...} é direct-list-initialization
T t = {...} é copy-list-initialization
- Em um tipo de classe que não é aggregate, se a lista de inicialização estiver vazia e houver um construtor padrão, ocorre value-initialization
- Caso contrário, os construtores são considerados pela overload resolution comum, e as sobrecargas de construtor com
std::initializer_list têm prioridade
- A inicialização com chaves não permite narrowing conversion ao inicializar com um único elemento
struct A {
const int x;
};
A b{4}; // possível
A c{4.0f}; // erro de narrowing conversion
A inicialização com parênteses é mais permissiva
- A inicialização com parênteses, como
T t(a, b, c);, chama direct-non-list-initialization e possui regras parecidas com direct-list-initialization, mas diferentes
T t(); não é criação de objeto; é interpretado como uma declaração de função sem argumentos e tipo de retorno T, o problema conhecido como most vexing parse
- A partir do C++20, aggregates também podem usar inicialização com parênteses
- A aggregate initialization baseada em parênteses se comporta de forma diferente da inicialização baseada em chaves
- Permite narrowing conversion
- Os elementos restantes são diretamente value-initialized, não inicializados com lista vazia
- Não estende o tempo de vida de objetos temporários vinculados a referências
struct T {
const int& r;
};
T t(42);
- No código acima,
t.r se torna uma dangling reference, e lê-la é undefined behaviour
std::initializer_list, construtor de cópia e elision
- A inicialização com parênteses pode chamar o construtor de cópia de
T mesmo quando há uma sobrecarga de construtor com std::initializer_list<T>
- Por outro lado, na inicialização com chaves, o construtor
std::initializer_list pode ter prioridade
struct T {
T(std::initializer_list<T>) {
std::cout << "list" << std::endl;
}
T(const T&) {
std::cout << "copy" << std::endl;
}
};
T t{}; // list
T s{t}; // list
T r(t); // copy
T q(T{}); // list, sem cópia
- O fato de não ocorrer cópia em
T q(T{}); se deve à regra de que, em direct- e copy-initialization, se o initializer for um prvalue do tipo T, o objeto é inicializado diretamente por essa expressão
- Esse comportamento costuma ser conhecido como copy elision, mas o padrão não o chama explicitamente de elision
- A descrição do padrão é insuficiente quanto a se a mesma elision é possível em list-initialization, e a CWG issue 2311 está relacionada a isso
- GCC e Clang realizam elision na maioria dos casos
- Quando há uma sobrecarga de construtor com
std::initializer_list<T>, o GCC usa esse construtor em vez de realizar elision
Ordem de avaliação e conclusão prática
- A ordem de avaliação dos elementos de uma lista de inicialização com parênteses não é garantida
- A lista de inicialização com chaves avalia os elementos estritamente da esquerda para a direita
- Há também regras especiais, como inicialização de variáveis estáticas e constant initialization, mas só a inicialização de objetos comuns já torna as regras suficientemente complexas
- Na prática, é mais seguro escrever construtores explícitos que deixem clara a intenção de inicialização, em vez de depender de construtores padrão implícitos e das regras de inicialização
1 comentários
Opiniões do Hacker News
A explicação de que, ao inicializar por valor
T, o resultado vira 0 estava correta; no começo isso passou despercebido, mas o autor de fato estava inicializandoxpor valor.No geral, acho que as regras de inicialização do C++ foram ampliadas e remendadas ao longo de 40 anos, criando cantos difíceis e contraintuitivos, mas em cerca de 99,9% dos casos elas funcionam como se espera.
Acho que a grande melhoria seria tornar a inicialização padrão explícita e, nos demais casos, sempre fazer inicialização por valor. Quando se quiser evitar o custo de preencher arrays grandes com zeros, seria melhor explicitar isso com uma sintaxe como
std::array = void;.Parece um caso de alguém tentando ser esperto demais e depois reclamando que ficou difícil de lidar.
A abordagem inteira está errada. Não coloque uma referência const dentro de uma struct; se for realmente necessário, o correto é usar
std::reference_wrapper.A resposta é um pouco ríspida, mas o ponto central é que a conclusão do texto está completamente errada. Não escreva construtores manualmente; siga a regra dos 5/3/0 e, se precisar manter uma referência const, verifique se está passando um objeto temporário rvalue. Isso não é um problema tão assustador assim.
Mesmo alguém que só passou por tutoriais básicos simplesmente evitaria esses cenários artificiais.
std::reference_wrapper, nem me lembro de tê-lo visto em várias bases de código C++ em que trabalhei.Ele provavelmente aparece em algum lugar de código de templates profundo e complexo, e a afirmação pode até estar correta, mas, pela minha experiência, isso não é conhecimento comum.
O texto original de
I Have No Mouth, and I Must Scream (1967)pode ser visto aqui: https://talesofmytery.blogspot.com/2018/10/harlan-ellison-i-...A família de Martin H Greenberg estava hospedada na nossa casa, e ele estava procurando Martin. Eu gostava de ficção científica e já tinha lido alguns livros dele; depois que ouvi o nome, a conversa ficou quase toda borrada na memória. No fim, acordei Martin e passei o telefone para ele, e depois disso foi difícil voltar a dormir.
Fiquei surpreso por não haver nenhum comentário sarcástico sobre a parte em que
unlessaparece até em negrito.Há sintaxes bonitas como
T t{v0};eT t{v0, v1};, mas a inicialização com 1 elemento não se comporta de forma tão confiável quanto nos casos com 2 ou mais elementos.É estranho ver essa diferença numa linguagem que caminha para facilitar a criação de structs a partir de packs de parâmetros e que também dá suporte a algo parecido com arrays de tamanho variável inicializáveis desse jeito. Naturalmente, o tipo também pode ser um template.
Você pode escrever um construtor manualmente, e também pode inicializar uma tupla ou um array fornecendo apenas um elemento, mas, em certos casos especiais, um construtor errado pode ser chamado. Descobri isso quando as listas de inicialização do C++11 tinham acabado de sair e achei uma loucura.
Para ver mais esquisitices do C++, recomendo o C++ FQA: https://yosefk.com/c++fqa/
Ele já tem uns 15 anos, mas, como o C++ quase nunca remove recursos ou comportamentos antigos, ao mesmo tempo continua atemporal.
O tema do blog é realmente bonito. Parece claramente inspirado em computadores da era DEC, mas ainda assim é limpo e minimalista, o que dá uma sensação nova.
Horrível. É uma das partes assustadoras do C++. É ainda mais lamentável porque a linguagem tem recursos excelentes e gente brilhante trabalhando nela.
Espero que algo como o C++ syntax 2 do Herb transforme isso numa linguagem que pessoas comuns como eu consigam usar.
Os exemplos do texto foram feitos para atingir deliberadamente casos de borda de um recurso em que a linguagem gera automaticamente, como exceção, funções-membro especiais que o programador optou por não escrever manualmente.
O C++ tem como objetivo de projeto pagar apenas pelo que se usa, e a regra dos 3/regra dos 5 é conhecimento básico de nível introdutório em C++. Como essas funções-membro especiais só são geradas pelo compilador sob certas condições, também é básico saber que, se as condições não forem atendidas, elas não serão geradas automaticamente.
No fim, não sei se é tão absurdo assim saber que você precisa configurar diretamente o construtor que vai usar. Também não deveria ser surpreendente que uma instância precise ser inicializada ao ser criada. No mundo real, as pessoas trabalham assim sem fazer grande alarde.
Ler isso me dá tontura. Lembro da época em que eu tentava entender construtores e inicialização de objetos em Java
Pelo menos pela minha experiência até agora, a escolha de não ter construtores especiais, como em Go e Rust, simplifica muita coisa
Fico curioso se existe alguém que, depois de migrar para uma linguagem sem construtores, sentiu falta deles
Claro, dá para escrever manualmente os campos um a um em
MaybeUninitcomptr::write, mas não é ergonômico, e a struct precisa permitir isso explicitamenteNo fim, isso significa que todos os construtores precisam ser declarados e definidos explicitamente, e C++ oferece essa opção desde o começo. A geração automática de funções-membro especiais é um recurso adicionado para funcionar apenas em casos bem específicos, para quem quer evitar código repetitivo
Se quiser, basta adicionar seus próprios construtores e levar uma vida normal. Reclamar que funções-membro especiais tornam coisas simples complicadas é parecido com reclamar que o inglês não é simples porque existem palavras difíceis no dicionário
Pelo que entendo, construtores em Rust não são basicamente parecidos com os de Java?
https://codefibershq.com/blog/golang-why-nil-is-not-always-n...
Fico curioso se existe alguma ferramenta de C++ que adicione ou mostre todos os comportamentos implícitos que acontecem por trás
Por exemplo, uma ferramenta que mostre construtores adicionados automaticamente, construtores de cópia implícitos e outras surpresas do tipo
cppinsightsNo caso de
T::T() = default;, o exemplo em que você esperaria que a saída fosse 0, mas acaba sendo lixo, na verdade não é tão estranho assimLink relacionado: https://consteval.ca/2024/07/03/initialization/#:~:text=You%...
Isso porque, se fosse feito de outro jeito, qualquer usuário da biblioteca poderia alterar o comportamento dela