
É sabido que a semântica de inicialização é uma das partes mais complexas do C ++. Existem muitos tipos de inicialização, descritos por sintaxe diferente, e todos interagem de maneira complexa e desafiadora. O C ++ 11 trouxe o conceito de "inicialização universal". Infelizmente, ela introduziu regras ainda mais complexas e, por sua vez, elas foram bloqueadas no C ++ 14, C ++ 17 e alteradas novamente no C ++ 20.
Sob o corte - vídeo e tradução do relatório de Timur Doumler da conferência C ++ na Rússia . O Timur primeiro resume os resultados históricos da evolução da inicialização em C ++, fornece uma visão geral sistemática da versão atual da regra de inicialização, problemas e surpresas típicos, explica como usar todas essas regras com eficiência e, finalmente, fala sobre novas propostas no padrão que podem tornar a semântica de inicialização C ++ 20 é um pouco mais conveniente. Além disso, a história está em seu nome.
Sumário

O gif que você vê agora transmite muito bem a mensagem principal do relatório. Eu o encontrei na Internet há cerca de seis meses e publiquei no meu Twitter. Nos comentários, alguém disse que faltam mais três tipos de inicialização. Começou uma discussão, durante a qual fui convidado a relatar isso. E assim tudo começou.
Sobre a inicialização Nikolay Yossutis já disse . Seu relatório incluiu uma lista de slides de 19 maneiras diferentes de inicializar um int:
int i1;
Parece-me que esta é uma situação única para uma linguagem de programação. A inicialização de uma variável é uma das ações mais simples, mas em C ++ não é nada fácil de fazer. É improvável que esse idioma tenha qualquer outra área em que, nos últimos anos, houvesse tantos relatórios de desvios do padrão, correções e mudanças. As regras de inicialização mudam de padrão para padrão e há inúmeras postagens na Internet sobre como inicializar em C ++. Portanto, fazer uma revisão sistemática é uma tarefa não trivial.
Apresentarei o material em ordem cronológica: primeiro falaremos sobre o que foi herdado de C, depois sobre C ++ 98, depois sobre C ++ 03, C ++ 11, C ++ 14 e C ++ 17. Discutiremos erros comuns e darei minhas recomendações sobre a inicialização adequada. Também falarei sobre inovações em C ++ 20. Uma tabela de visão geral será apresentada no final do relatório.
Inicialização padrão (C)
No C ++, muitas coisas são herdadas do C, é por isso que vamos começar com ele. Existem várias maneiras de inicializar variáveis em C. Eles podem não ser inicializados e isso é chamado de inicialização padrão . Na minha opinião, este é um nome infeliz. O fato é que nenhuma variável recebe um valor padrão, ela simplesmente não é inicializada. Se você ativar uma variável não inicializada em C ++ e C, terá um comportamento indefinido:
int main() { int i; return i;
O mesmo se aplica aos tipos personalizados: se em alguma struct
houver campos não inicializados, ao acessá-los, também ocorrerá um comportamento indefinido:
struct Widget { int i; int j; }; int main() { Widget widget; return widget.i;
Muitas novas construções foram adicionadas ao C ++: classes, construtores, métodos públicos, privados, mas nada disso afeta o comportamento descrito. Se algum elemento não for inicializado na classe, ao acessá-lo, ocorrerá um comportamento indefinido:
class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i();
Não há maneira mágica de inicializar um elemento de classe em C ++ por padrão. Esse é um ponto interessante e, durante os primeiros anos de minha carreira com C ++, eu não sabia disso. Nem o compilador nem o IDE, que eu usei na época, me lembraram disso de forma alguma. Meus colegas não prestaram atenção a esse recurso ao verificar o código. Tenho certeza de que, por causa dela, existem alguns bugs muito estranhos no meu código escritos durante esses anos. Pareceu-me óbvio que as classes deveriam inicializar suas variáveis.
No C ++ 98, você pode inicializar variáveis usando a lista de inicializadores de membros. Mas essa solução para o problema não é ideal, pois deve ser feita em cada construtor, e isso é fácil de esquecer. Além disso, a inicialização continua na ordem em que as variáveis são declaradas e não na ordem da lista de inicializadores de membros:
No C ++ 11, foram adicionados inicializadores diretos de membros, que são muito mais convenientes de usar. Eles permitem que você inicialize todas as variáveis ao mesmo tempo, e isso garante que todos os elementos sejam inicializados:
Minha primeira recomendação: sempre que puder, use sempre o DMI (inicializadores diretos de membros). Eles podem ser usados tanto com tipos float
( float
e int
) quanto com objetos. O hábito de inicializar elementos nos leva a abordar essa questão de maneira mais consciente.
Inicialização de cópia (C)
Portanto, o primeiro método de inicialização herdado de C é a inicialização por padrão e não deve ser usado. A segunda maneira é a inicialização da cópia . Nesse caso, indicamos a variável e através do sinal de igual - seu valor:
A inicialização de cópia também é usada quando um argumento é passado para uma função por valor ou quando um objeto é retornado de uma função por valor:
Um sinal de igual pode dar a impressão de que um valor está sendo atribuído, mas não é assim. A inicialização da cópia não é uma atribuição de valor. Não haverá nada sobre apropriação neste relatório.
Outra propriedade importante da inicialização da cópia: se os tipos de valores não corresponderem, uma sequência de conversão será executada. Uma sequência de conversão possui certas regras, por exemplo, não chama construtores explícitos, pois eles não estão transformando construtores. Portanto, se você executar a inicialização de cópia para um objeto cujo construtor está marcado como explícito, ocorrerá um erro de compilação:
struct Widget { explicit Widget(int) {} }; Widget w1 = 1;
Além disso, se houver outro construtor que não seja explícito, mas com pior tipo, a inicialização de cópia o chamará, ignorando o construtor explícito:
struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1;
Inicialização agregada (C)
O terceiro tipo de inicialização que eu gostaria de falar é a inicialização agregada . É executado quando a matriz é inicializada com uma série de valores entre chaves:
int i[4] = {0, 1, 2, 3};
Se você não especificar o tamanho da matriz, será derivado do número de valores entre colchetes:
int j[] = {0, 1, 2, 3};
A mesma inicialização é usada para classes agregadas, ou seja, classes que são apenas uma coleção de elementos públicos (existem mais algumas regras na definição de classes agregadas, mas agora não vamos nos deter sobre elas):
struct Widget { int i; float j; }; Widget widget = {1, 3.14159};
Essa sintaxe funcionou mesmo em C e C ++ 98 e, começando com C ++ 11, você pode pular o sinal de igual:
Widget widget{1, 3.14159};
Inicialização agregada, na verdade, usa inicialização de cópia para cada elemento. Portanto, se você tentar usar a inicialização agregada (com um sinal de igual e sem ele) para vários objetos com construtores explícitos, a inicialização da cópia será executada para cada objeto e ocorrerá um erro de compilação:
struct Widget { explicit Widget(int) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4};
E se houver outro construtor para esses objetos, não explícito, ele será chamado, mesmo que seja mais adequado para o tipo:
struct Widget { explicit Widget(int) {} Widget(double) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4};
Vamos considerar mais uma propriedade da inicialização agregada. Pergunta: qual valor esse programa retorna?
struct Widget { int i; int j; }; int main() { Widget widget = {1}; return widget.j; }
Texto ocultoIsso mesmo, zero. Se você pular alguns elementos em uma matriz de valores durante a inicialização agregada, as variáveis correspondentes serão definidas como zero. Essa é uma propriedade muito útil, porque, graças a ela, nunca pode haver elementos não inicializados. Funciona com classes agregadas e com matrizes:
Outra propriedade importante da inicialização agregada é a omissão de colchetes (brision elision). Que valor você acha que esse programa retorna? Ele possui um Widget
, que é um agregado de dois valores int
, e Thingy
, um agregado de Widget
e int
. O que obtemos se passarmos dois valores de inicialização para ele: {1, 2}
?
struct Widget { int i; int j; }; struct Thingy { Widget w; int k; }; int main() { Thingy t = {1, 2}; return tk;
Texto ocultoA resposta é zero. Aqui estamos lidando com um subagregado, ou seja, com uma classe agregada aninhada. Essas classes podem ser inicializadas usando colchetes aninhados, mas você pode pular um desses pares de colchetes. Nesse caso, é realizada uma passagem recursiva do subagregado e {1, 2}
é equivalente a {{1, 2}, 0}
. É certo que essa propriedade não é totalmente óbvia.
Inicialização estática (C)
Finalmente, a inicialização estática também é herdada de C: variáveis estáticas são sempre inicializadas. Isso pode ser feito de várias maneiras. Uma variável estática pode ser inicializada com uma expressão constante. Nesse caso, a inicialização ocorre no momento da compilação. Se você não atribuir nenhum valor à variável, ele será inicializado com zero:
static int i = 3;
Este programa retorna 3 mesmo que j
não j
inicializado. Se a variável for inicializada não por uma constante, mas por um objeto, poderão surgir problemas.
Aqui está um exemplo de uma biblioteca real em que eu estava trabalhando:
static Colour red = {255, 0, 0};
Havia uma classe Color nele, e as cores primárias (vermelho, verde, azul) eram definidas como objetos estáticos. Essa é uma ação válida, mas assim que outro objeto estático aparece no inicializador do qual é usado o red
, a incerteza aparece porque não há uma ordem rígida na qual as variáveis são inicializadas. Seu aplicativo pode acessar uma variável não inicializada e ela falha. Felizmente, no C ++ 11, tornou-se possível usar o construtor constexpr
, e depois lidamos com a inicialização constante. Nesse caso, não há problemas com a ordem de inicialização.
Portanto, quatro tipos de inicialização são herdados da linguagem C: inicialização padrão, cópia, agregação e inicialização estática.
Inicialização direta (C ++ 98)
Vamos para o C ++ 98. Talvez o recurso mais importante que distingue C ++ de C sejam os construtores. Aqui está um exemplo de uma chamada de construtor:
Widget widget(1, 2); int(3);
Usando a mesma sintaxe, você pode inicializar tipos int
como int
e float
. Essa sintaxe é chamada inicialização direta . É sempre executado quando temos um argumento entre parênteses.
Para tipos int
( int
, bool
, float
), não há diferença em relação à inicialização de cópia aqui. Se estamos falando de tipos de usuário, ao contrário da inicialização de cópia, com inicialização direta, você pode passar vários argumentos. Na verdade, por causa disso, a inicialização direta foi inventada.
Além disso, uma inicialização direta não executa uma sequência de conversão. Em vez disso, o construtor é chamado usando a resolução de sobrecarga. A inicialização direta tem a mesma sintaxe que uma chamada de função e usa a mesma lógica que outras funções do C ++.
Portanto, na situação com um construtor explícito, a inicialização direta funciona bem, embora a inicialização de cópia gere um erro:
struct Widget { explicit Widget(int) {} }; Widget w1 = 1;
Em uma situação com dois construtores, um dos quais é explícito e o segundo é menos adequado no tipo, o primeiro é chamado com inicialização direta e o segundo é chamado com a cópia. Nessa situação, alterar a sintaxe levará a uma chamada para outro construtor - isso geralmente é esquecido:
struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1;
A inicialização direta sempre é usada quando parênteses são usados, inclusive quando a notação de chamada do construtor é usada para inicializar um objeto temporário, bem como em new
expressões com um inicializador entre colchetes e expressões de cast
:
useWidget(Widget(1, 2));
Essa sintaxe existe enquanto o próprio C ++ existir e possui uma falha importante que Nikolai mencionou em seu discurso principal: a análise mais irritante . Isso significa que tudo o que o compilador pode ler como uma declaração (declaração), ele lê exatamente como uma declaração.
Considere um exemplo no qual há uma classe Widget
e uma classe Thingy
e um construtor Thingy
que recebe um Widget
:
struct Widget {}; struct Thingy { Thingy(Widget) {} }; int main () { Thingy thingy(Widget()); }
À primeira vista, parece que após a inicialização do Thingy
, o Widget
padrão criado é passado para ele, mas, de fato, a função é declarada aqui. Este código declara uma função que recebe outra função como entrada, que não recebe nada como entrada e retorna um Widget
, e a primeira função retorna Thingy
. O código é compilado sem erros, mas é improvável que tenhamos procurado esse comportamento.
Inicialização de valor (C ++ 03)
Vamos para a próxima versão - C ++ 03. É geralmente aceito que não houve alterações significativas nesta versão, mas não é assim. No C ++ 03, apareceu a inicialização do valor, na qual são escritos parênteses vazios:
int main() { return int();
No C ++ 98, o comportamento indefinido ocorre aqui porque a inicialização ocorre por padrão e, iniciando no C ++ 03, este programa retorna zero.
A regra é a seguinte: se houver um construtor padrão definido pelo usuário, a inicialização com um valor chamará esse construtor, caso contrário, zero será retornado.
Vamos considerar com mais detalhes a situação com o construtor personalizado:
struct Widget { int i; }; Widget get_widget() { return Widget();
Neste programa, a função inicializa o valor para o novo Widget
e o retorna. Chamamos essa função e acessamos o elemento i
do objeto Widget
. Desde C ++ 03, o valor de retorno aqui é zero, pois não há construtor padrão definido pelo usuário. E se esse construtor existe, mas não inicializa i
, obtemos um comportamento indefinido:
struct Widget { Widget() {}
Vale ressaltar que “definido pelo usuário” não significa “definido pelo usuário”. Isso significa que o usuário deve fornecer o corpo do construtor, ou seja, chaves. Se no exemplo acima, substitua o corpo do construtor por = default
(esse recurso foi adicionado no C ++ 11), o significado do programa será alterado. Agora, como temos um construtor definido pelo usuário (definido pelo usuário), mas não fornecido pelo usuário (fornecido pelo usuário), o programa retorna zero:
struct Widget { Widget() = default;
Agora vamos tentar Widget() = default
da classe. O significado do programa mudou novamente: Widget() = default
é considerado um construtor fornecido pelo usuário se estiver fora da classe. O programa retorna comportamento indefinido novamente.
struct Widget { Widget(); int i; }; Widget::Widget() = default;
Existe uma certa lógica: um construtor definido fora de uma classe pode estar dentro de outra unidade de tradução. O compilador pode não ver esse construtor, pois pode estar em outro arquivo .cpp
. Portanto, o compilador não pode tirar conclusões sobre esse construtor e não pode distinguir um construtor com um corpo de um construtor com = default
.
Inicialização universal (C ++ 11)
Houve muitas mudanças muito importantes no C ++ 11. Em particular, a uniformização universal foi introduzida, que eu prefiro chamar de “inicialização de unicórnio” porque é apenas mágica. Vamos ver por que ela apareceu.
Como você já notou, no C ++ existem muitas sintaxes de inicialização diferentes com comportamentos diferentes. A análise irritante entre parênteses causou muitos inconvenientes. Os desenvolvedores também não gostaram que a inicialização agregada pudesse ser usada apenas com matrizes, mas não com contêineres como std::vector
. Em vez disso, você tinha que executar .reserve
e .push_back
, ou usar todos os tipos de bibliotecas assustadoras:
Os criadores da linguagem tentaram resolver todos esses problemas, introduzindo sintaxe com chaves, mas sem sinal de igual. Supunha-se que essa seria uma sintaxe única para todos os tipos, nos quais chaves são usadas e não há nenhum problema de análise irritante. Na maioria dos casos, essa sintaxe faz seu trabalho.
Essa nova inicialização é chamada de inicialização de lista e vem em dois tipos: direta e cópia. No primeiro caso, apenas chaves são usadas, no segundo - chaves com um sinal de igual:
A lista usada para inicialização é chamada braced-init-list . É importante que esta lista não seja um objeto, ela não tem tipo. Mudar para o C ++ 11 de versões anteriores não cria problemas com tipos agregados, portanto, essa alteração não é crítica. Mas agora a lista entre chaves tem novos recursos. Embora não tenha um tipo, ele pode ser oculto convertido em std::initializer_list
, mas é um tipo novo e especial. E se houver um construtor que aceite std::initializer_list
como entrada, esse construtor será chamado:
template <typename T> class vector {
Parece-me que, do lado do comitê C ++, std::initializer_list
não era a solução mais bem-sucedida. Dele mais mal do que bem.
Para começar, std::initializer_list
é um vetor de tamanho fixo com elementos const
. Ou seja, é um tipo, possui funções de begin
e end
que os iteradores retornam, possui seu próprio tipo de iterador e, para usá-lo, é necessário incluir um cabeçalho especial. Como os elementos std::initializer_list
são const
, ele não pode ser movido; portanto, se T
no código acima é do tipo somente movimento, o código não será executado.
Em seguida, std::initializer_list
é um objeto. Com ele, criamos e transferimos objetos. Como regra, o compilador pode otimizar isso, mas do ponto de vista da semântica, ainda lidamos com objetos desnecessários.
Alguns meses atrás, houve uma pesquisa no Twitter: se você pudesse voltar no tempo e remover algo do C ++, o que você removeria? A maioria de todos os votos recebeu exatamente initializer_list
.
https://twitter.com/shafikyaghmour/status/1058031143935561728
, initializer_list
. , .
, . , initializer_list
, . :
std::vector<int> v(3, 0);
vector
int
, , , — . . , initializer_list
, 3 0.
:
std::string s(48, 'a');
48 «», «0». , string
initializer_list
. 48 , . ASCII 48 — «0». , , , int
char
. . , , .
. , ? ?
template <typename T, size_t N> auto test() { return std::vector<T>{N}; } int main () { return test<std::string, 3>().size(); }
, — 3. string
int
, 1, std::vector<std::int>
initializer_list
. initializer_list
, . string
int
float
, , . , . , emplace , . , {}
.
, .
.
— ( {a}
)
( = {a}
);
:
- «» ,
std::initializer_list
.
— . - ,
()
.
.
1: = {a}
, a
,
.
2: , {}
.
, initializer_list
.
Widget<int> widget{}\
?
template Typename<T> struct Widget { Widget(); Widget(std::initializer_list<T>); }; int main() { Widget<int> widget{};
, , initializer_list
, initializer_list
. . , , initializer_list
. , . , .
{}
. , -, , Widget() = default
Widget() {}
— .
Widget() = default
:
struct Widget { Widget() = default; int i; }; int main() { Widget widget{};
Widget() {}
:
struct Widget { Widget() {};
: , (narrowing conversions). int
double
, , :
int main() { int i{2.0};
, double
. C++11, , . :
struct Widget { int i; int j; }; int main() { Widget widget = {1.0, 0.0};
, , , , (brace elision). , , . , map
. map
, — :
std::map<std::string, std::int> my_map {{"abc", 0}, {"def", 1}};
, . :
std::vector<std::string> v1 {"abc", "def"};
, , initializer_list
. initializer_list
, , , . , . , .
initializer_list
— initializer_list
, . , const char*
. , string
, char
. . , , .
:
. braced-init-list . :
Widget<int> f1() { return {3, 0};
, , braced-init-list . braced-init-list , .
, . StackOverflow , . , . , , :
#include <iostream> struct A { A() {} A(const A&) {} }; struct B { B(const A&) {} }; void f(const A&) { std::cout << "A" << std::endl; } void f(const B&) { std::cout << "B" << std::endl; } int main() { A a; f( {a} ); // A f( {{a}} ); // ambiguous f( {{{a}}} ); // B f({{{{a}}}}); // no matching function }
++14
, C++11 . , , . C++14. , .
, ++11 direct member initializers, . , direct member initializers . ++14, direct member initializers:
struct Widget { int i = 0; int j = 0; }; Widget widget{1, 2};
, auto
. ++11 auto
braced-init-list, std::initializer_list
:
int i = 3;
: auto i{3}
, int
, std::initializer_list<int>
. ++14 , auto i{3}
int
. , . , auto i = {3}
std::initializer_list<int>
. , : int
, — initializer_list
.
auto i = 3;
, C++14 , , , , . , .
, ++14 :
, , std::initializer_list
.
std::initializer_list
move-only .
c , emplace
make_unique
.
, :
, , .
: assert(Widget(2,3))
, assert(Widget{2,3})
. , , , . , . .
C++
, ++.
int
, . . — , .
: , , std::initializer_list
, direct member initializers. , .
, é . .
struct Point { int x = 0; int y = 0; }; setPosition(Point{2, 3}); takeWidget(Widget{});
braced-init-list — .
setPosition({2, 3}); takeWidget({});
, , . , — , . , , , , , . , , initializer_list
. : , , .
:
= value
= {args}
= {}
:
std::initializer_list
- direct member initialisation (
(args)
)
{args}
{}
é
(args)
, (args)
vexing parse. . 2013 , , auto
. , : auto i;
— . , :
auto widget = Widget(2, 3);
, . , , vexing parse:
auto thingy = Thingy();
« auto» («almost always auto», AAA), ++11 ++14 , , , std::atomic<int>
:
auto count = std::atomic<int>(0);
, atomic . , , , , . ++17 , , (guaranteed copy elision):
auto count = std::atomic<int>(0);
auto
. — direct member initializers. auto
.
++17 CTAD (class template argument deduction). , . . , CppCon, CTAD , . , ++17 , ++11 ++14, , . , , , , .
(++20)
++20, . , , : (designated initialization):
struct Widget { int a; int b; int c; }; int main() { Widget widget{.a = 3, .c = 7}; };
, . , , . , .
, b
.
, , , . , .
, , 99, :
, , . ++ , , . :
Widget widget{.c = 7, .a = 3};
, .
++ , {.ce = 7};
, {.c{.e = 7}}
:
Widget widget{.ce = 7};
++ , , :
Widget widget{.a = 3, 7};
++ . , -, , .
int arr[3]{.[1] = 7};
C++20
++20 , . ( wg21.link/p1008 ).
++17 , , . , , , :
struct Widget { Widget() = delete; int i; int j; }; Widget widget1;
, , . ++20 . , . , . , , , .
( wg21.link/p1009 ). Braced-init-list new
, : , ? — , : braced-init-list new
:
double a[]{1, 2, 3};
, ++11 braced-init-list. ++ . , .
(C++20)
, ++20 . , . ++20 : ( wg21.link/p0960 ).
struct Widget { int i; int j; }; Widget widget(1, 2);
. , emplace
make_unique
. . : auto
, : 58.11 .
struct Widget { int i; int j; }; auto widget = Widget(1, 2);
, :
int arr[3](0, 1, 2);
, : uniform 2.0. . , , , , . — initializer_list
: , , — . , . , - , — . .
, . direct member initializers. auto
. direct member initializers — , . , . — , .
, , . — , — . , .

, , C++ Russia 2019 Piter «Type punning in modern C++» . , ++20, , , «» ++ , .