Por que a inicialização de lista (usando chaves) é melhor que as alternativas?

406
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 no SO, então deixe-me responder minha própria pergunta.

Oleksiy
fonte
12
Por que não usar auto?
Mark Garcia
33
Isso é verdade, é conveniente, mas reduz a legibilidade na minha opinião - eu gosto de ver que tipo de objeto é ao ler código. Se você tem 100% de certeza de que tipo do objeto é, por que usar auto? E se você usar a inicialização da lista (leia minha resposta), pode ter certeza de que ela está sempre correta.
precisa saber é
103
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratorgostaria de uma palavra com você.
Xeo 14/08/13
9
@Oleksiy Eu recomendo a leitura deste GotW .
Rapptz 14/08/13
17
@doc Eu diria que using MyContainer = std::map<std::string, std::vector<std::string>>;é ainda melhor (especialmente como você pode molde ele!)
JAB

Respostas:

357

Basicamente, copia e cola do "The C ++ Programming Language 4th Edition" de Bjarne Stroustrup :

A inicialização da lista não permite restringir (§iso.8.5.4). Isso é:

  • Um número inteiro não pode ser convertido para outro número inteiro que não pode conter seu valor. Por exemplo, char para int é permitido, mas não int para char.
  • Um valor de ponto flutuante não pode ser convertido para outro tipo de ponto flutuante que não pode conter seu valor. Por exemplo, é permitido flutuar para dobrar, mas não dobrar 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 a {} é ao usar a autopalavra-chave para obter o tipo determinado pelo inicializador.

Exemplo:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

Conclusão

Prefira a inicialização {} sobre as alternativas, a menos que você tenha um forte motivo para não fazê-lo.

Oleksiy
fonte
52
Também existe o fato de que o uso ()pode ser analisado como uma declaração de função. É confuso e inconsistente que você pode dizer, T t(x,y,z);mas não é T t(). E, às vezes, com certeza x, você nem pode dizer T t(x);.
Juanchopanza
84
Eu discordo totalmente desta resposta; inicialização inicial torna-se uma bagunça completa quando você tem tipos com um ctor aceitando a std::initializer_list. O RedXIII menciona esse problema (e apenas o ignora), enquanto você o ignora completamente. A(5,4)e A{5,4}pode chamar funções completamente diferentes, e isso é uma coisa importante a saber. Pode até resultar em chamadas que parecem não intuitivas. Dizer que você deve preferir {}por padrão fará com que as pessoas não entendam o que está acontecendo. Isso não é sua culpa, no entanto. Pessoalmente, acho que é um recurso extremamente mal pensado.
user1520427
13
@ user1520427 É por isso que existe a parte "a menos que você tenha um forte motivo para não participar".
27615 Oleksiy
67
Embora essa pergunta seja antiga, ela tem muitos acessos, portanto, estou adicionando isso aqui apenas para referência (não a vi em nenhum outro lugar da página). A partir do C ++ 14 com as novas Regras para dedução automática a partir de braced-init-list , agora é possível escrever auto var{ 5 }e será deduzido como intnão std::initializer_list<int>.
Edoardo Sparkon Dominici
13
Haha, de todos os comentários ainda não está claro o que fazer. O que está claro é que a especificação C ++ está uma bagunça!
Drumm
113

Já existem ótimas respostas sobre as vantagens de usar a inicialização de lista, no entanto, minha regra pessoal NÃO é usar chaves sempre que possível, mas torná-la dependente do significado conceitual:

  • Se o objeto que estou criando conceitualmente mantém os valores que estou passando no construtor (por exemplo, contêineres, estruturas de POD, atômica, ponteiros inteligentes etc.), então estou usando os chavetas.
  • Se o construtor se assemelhar a uma chamada de função normal (ele executa algumas operações mais ou menos complexas que são parametrizadas pelos argumentos), então estou usando a sintaxe da chamada de função normal.
  • Para inicialização padrão, eu sempre uso chaves.
    Por um lado, dessa forma, eu sempre tenho certeza de que o objeto é inicializado, independentemente de, por exemplo, ser uma classe "real" com um construtor padrão que seria chamado assim mesmo ou um tipo interno / POD. Segundo, é - na maioria dos casos - consistente com a primeira regra, pois um objeto inicializado padrão geralmente representa um objeto "vazio".

Na minha experiência, esse conjunto de regras pode ser aplicado de maneira muito mais consistente do que usar chaves, por padrão, mas ter que lembrar explicitamente todas as exceções quando elas não puderem ser usadas ou tiverem um significado diferente da sintaxe "normal" de chamada de função com parênteses (chama uma sobrecarga diferente).

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

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements
MikeMB
fonte
11
Concordo totalmente com a maioria das suas respostas. No entanto, você não acha que colocar chaves vazias para vetor é apenas redundante? Quero dizer, tudo bem, quando você precisa inicializar com valor um objeto do tipo T genérico, mas qual é o objetivo de fazê-lo para código não genérico?
27616 Mikhail
8
@ Mikhail: Certamente é redundante, mas é um hábito meu sempre tornar explícita a inicialização de variáveis ​​locais. Como eu escrevi, isso é principalmente sobre consistência, então não esqueço quando importa. Certamente não é nada que eu mencionaria em uma revisão de código ou colocaria em um guia de estilo.
27516 MikeMB
4
conjunto de regras bastante limpo.
laike9m
5
Esta é de longe a melhor resposta. {} é como herança - fácil de abusar, levando a códigos difíceis de entender.
UKMonkey
2
@MikeMB exemplo: const int &b{}<- não tenta criar uma referência não inicializada, mas a vincula a um objeto inteiro temporário. Segundo exemplo: struct A { const int &b; A():b{} {} };<- não tenta criar uma referência não inicializada (como ()faria), mas vincula-a a um objeto inteiro temporário e, em seguida, deixa-a pendente. O GCC, mesmo com -Wall, não adverte para o segundo exemplo.
Johannes Schaub - litb 18/11/1918
92

Existem MUITAS razões para usar a inicialização entre parênteses, mas você deve estar ciente de que o initializer_list<>construtor é preferido aos outros construtores , a exceção sendo o construtor padrão. Isso leva a problemas com construtores e modelos nos quais o Tconstrutor de tipos pode ser uma lista de inicializadores ou um ctor antigo simples.

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        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 classes, há poucas razões para não usar a lista do inicializador.

XIII vermelho
fonte
20
Este é um ponto muito importante na programação genérica. Ao escrever modelos, não use braced-init-lists (o nome do padrão { ... }), a menos que você queira a initializer_listsemântica (bem, e talvez para a construção padrão de um objeto).
Xeo 14/08/13
82
Sinceramente, não entendo por que a std::initializer_listregra existe - apenas acrescenta confusão e confusão ao idioma. O que há de errado em fazer Foo{{a}}se você deseja o std::initializer_listconstrutor? Isso parece muito mais fácil de entender do que ter std::initializer_listprecedência sobre todas as outras sobrecargas.
User1520427
5
+1 no comentário acima, porque é realmente uma bagunça, eu acho !! não é lógica; Foo{{a}}segue alguma lógica para mim muito mais do Foo{a}que se transforma em intializer lista de precedência (ups pode o usuário acha hm ...)
Gabriel
32
Basicamente, o C ++ 11 substitui uma bagunça por outra. Oh, desculpe, não o substitui - ele adiciona a ele. Como você pode saber se não encontra essas aulas? E se você começar sem o std::initializer_list<Foo> construtor, mas ele for adicionado à Fooclasse em algum momento para estender sua interface? Então os usuários da Fooclasse estão ferrados.
doc
10
.. quais são as "MUITAS razões para usar a inicialização entre chaves"? Esta resposta aponta um motivo ( initializer_list<>), que não qualifica quem diz que é o preferido, e depois menciona um bom caso em que NÃO é o preferido. O que estou sentindo falta de que ~ 30 outras pessoas (em 21/04/2016) foram úteis?
precisa saber é o seguinte
0

É mais seguro, desde que você não construa com o -Wno-estreitando, como o Google faz no Chromium. Se o fizer, é MENOS seguro. Sem esse sinalizador, os únicos casos inseguros serão corrigidos pelo C ++ 20.

Nota: A) Os colchetes são mais seguros porque não permitem o estreitamento. B) Os brackers encaracolados são menos seguros porque podem ignorar construtores particulares ou excluídos e chamar implicitamente marcadores explícitos.

Esses dois combinados significam que eles são mais seguros se o que está dentro são constantes primitivas, mas menos seguros se forem objetos (embora fixados no C ++ 20)

Allan Jensen
fonte
Tentei procurar no goldbolt.org para ignorar os construtores "explícitos" ou "particulares" usando o código de exemplo fornecido e tornando um ou outro privado ou explícito, e fui recompensado com os erros apropriados do compilador. Gostaria de fazer backup com algum código de exemplo?
precisa
Esta é a correção do problema proposto para o C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen
11
Se você editar sua resposta para mostrar quais versões do C ++ você está falando, ficarei feliz em mudar meu voto.
Mark Storer