Por que a boot da lista (usando chaves) é melhor que as alternativas?

MyClass a1 {a}; // clearer and less error-prone than the other three MyClass a2 = {a}; MyClass a3 = a; MyClass a4(a); 

Por quê?

Não consegui encontrar uma resposta em SO, então deixe-me responder minha própria pergunta.

Basicamente copiando e colando de “The C ++ Programming Language 4th Edition” de Bjarne Stroustrup:

A boot da lista não permite o estreitamento (§iso.8.5.4). Isso é:

  • Um inteiro não pode ser convertido em outro inteiro que não possa manter seu valor. Por exemplo, char para int é permitido, mas não int para char.
  • Um valor de ponto flutuante não pode ser convertido em outro tipo de ponto flutuante que não pode manter seu valor. Por exemplo, float to double é permitido, mas não o dobro para flutuar.
  • Um valor de ponto flutuante não pode ser convertido em um tipo inteiro.
  • Um valor inteiro não pode ser convertido em um tipo de ponto flutuante.

Exemplo:

 void fun(double val, int val2) { int x2 = val; // if val==7.9, x2 becomes 7 (bad) char c2 = val2; // if val2==1025, c2 becomes 1 (bad) int x3 {val}; // error: possible truncation (good) char c3 {val2}; // error: possible narrowing (good) char c4 {24}; // OK: 24 can be represented exactly as a char (good) char c5 {264}; // error (assuming 8-bit chars): 264 cannot be // represented as a char (good) int x4 {2.0}; // error: no double to int value conversion (good) } 

A única situação em que = é preferível em relação a {} é ao usar auto palavra-chave auto para obter o tipo determinado pelo inicializador.

Exemplo:

 auto z1 {99}; // z1 is an initializer_list auto z2 = 99; // z2 is an int 

Conclusão

Prefira {} a boot pelas alternativas, a menos que você tenha uma forte razão para não fazer isso.

Existem MUITAS razões para usar a boot de chaves, mas você deve estar ciente de que o construtor initializer_list<> é o preferido para os outros construtores , a exceção é o construtor padrão. Isso leva a problemas com construtores e modelos em que o construtor do tipo T pode ser uma lista inicializadora ou um antigo ctor.

 struct Foo { Foo() {} Foo(std::initializer_list) { std::cout << "initializer list" << std::endl; } Foo(const Foo&) { std::cout << "copy ctor" << std::endl; } }; int main() { Foo a; Foo b(a); // copy ctor Foo c{a}; // copy ctor (init. list element) + initializer list!!! } 

Supondo que você não encontre essas classs, há poucas razões para não usar a lista de iniciadores.

Já existem ótimas respostas sobre as vantagens de usar a boot de listas, no entanto, minha regra prática é NÃO usar chaves sempre que possível, mas torná-las dependentes do significado conceitual:

  • Se o object que estou criando conceitualmente contém os valores que estou passando no construtor (por exemplo, contêineres, estruturas POD, atomics, smart pointers etc.), então eu estou usando as chaves.
  • Se o construtor se assemelha a uma chamada de function normal (ele executa algumas operações mais ou menos complexas que são parametrizadas pelos argumentos), então eu estou usando a syntax de chamada de function normal.
  • Para a boot padrão, sempre uso chaves.
    Por um lado, dessa forma eu sempre tenho certeza que o object é inicializado, independentemente de se, por exemplo, é uma class “real” com um construtor padrão que seria chamado de qualquer maneira ou um tipo builtin / POD. Em segundo lugar, é – na maioria dos casos – consistente com a primeira regra, já que um object inicializado padrão geralmente representa um object “vazio”.

Na minha experiência, este conjunto de regras pode ser aplicado de forma muito mais consistente do que usar chaves por padrão, mas ter que lembrar explicitamente todas as exceções quando elas não podem ser usadas ou ter um significado diferente da syntax de chamada de function “normal” com parênteses (chama uma sobrecarga diferente).

Por exemplo, se encheckbox muito bem com tipos de biblioteca padrão como std::vector :

 vector a{10,20}; //Curly braces -> fills the vector with the arguments vector b(10,20); //Parentesis -> uses arguments to parameterize some functionality, vector c(it1,it2); //like filling the vector with 10 integers or copying a range. vector d{}; //empty braces -> default constructs vector, which is equivalent //to a vector that is filled with zero elements