Quando usar o inicializador entre chaves?

94

Em C ++ 11, temos essa nova sintaxe para inicializar classes que nos dá um grande número de possibilidades de como inicializar variáveis.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Para cada variável que declaro, tenho que pensar qual sintaxe de inicialização devo usar e isso diminui minha velocidade de codificação. Tenho certeza de que não foi essa a intenção de introduzir as chaves.

Quando se trata de código de modelo, alterar a sintaxe pode levar a diferentes significados, portanto, seguir o caminho certo é essencial.

Eu me pergunto se existe uma diretriz universal sobre a sintaxe que devemos escolher.

Helami
fonte
1
Um exemplo de comportamento não intencional de {} inicialização: string (50, 'x') vs string {50, 'x'} aqui
P i

Respostas:

64

Acho que o seguinte pode ser uma boa diretriz:

  • Se o valor (único) com o qual você está inicializando pretende ser o valor exato do objeto, use a =inicialização de copy ( ) (porque então, em caso de erro, você nunca invocará acidentalmente um construtor explícito, que geralmente interpreta o valor fornecido diferente). Em locais onde a inicialização da cópia não está disponível, veja se a inicialização da chave tem a semântica correta e, em caso afirmativo, use-a; caso contrário, use a inicialização entre parênteses (se isso também não estiver disponível, você está sem sorte de qualquer maneira).

  • Se os valores com os quais você está inicializando forem uma lista de valores a serem armazenados no objeto (como os elementos de um vetor / matriz ou parte real / imaginária de um número complexo), use a inicialização de chaves, se disponível.

  • Se os valores com os quais você está inicializando não são valores a serem armazenados, mas descrevem o valor / estado pretendido do objeto, use parênteses. Os exemplos são o argumento de tamanho de a vectorou o argumento do nome de arquivo de um fstream.

Celtschk
fonte
4
@ user1304032: Uma localidade não é uma string, portanto, você não usaria a inicialização de cópia. Uma localidade também não contém uma string (ela pode armazenar essa string como um detalhe de implementação, mas esse não é seu propósito), portanto, você não usaria a inicialização de chaves. Portanto, a diretriz diz para usar a inicialização entre parênteses.
celtschk
2
Eu pessoalmente gostei dessa diretriz e ela também funciona bem em código genérico. Existem algumas exceções ( T {}ou motivos sintáticos, como a análise mais incômoda ), mas em geral acho que esse é um bom conselho. Observe que esta é minha opinião subjetiva, portanto, deve-se dar uma olhada nas outras respostas também.
helami
2
@celtschk: Isso não funcionará para tipos não copiáveis ​​e não móveis; type var{};faz.
ildjarn
2
@celtschk: Não estou dizendo que é algo que ocorreria com frequência, mas é menos digitação e funciona em mais contextos, então qual é a desvantagem?
ildjarn,
2
Minhas diretrizes certamente nunca pedem inicialização de cópia. ; -]
ildjarn
26

Tenho certeza de que nunca haverá uma diretriz universal. Minha abordagem é usar sempre chaves, lembrando que

  1. Os construtores da lista de inicializadores têm precedência sobre outros construtores
  2. Todos os contêineres de biblioteca padrão e std :: basic_string possuem construtores de lista de inicializadores.
  3. A inicialização do Curly Brace não permite conversões de estreitamento.

Portanto, as chaves redondas e as chaves não são intercambiáveis. Mas saber onde eles diferem me permite usar a inicialização de colchetes curvos sobre colchetes na maioria dos casos (alguns dos casos em que não consigo são erros de compilador).

Juanchopanza
fonte
6
Os colchetes têm a desvantagem de poder chamar o construtor de lista por engano. Os colchetes não. Não é uma razão para usar os colchetes por padrão?
helami
4
@user: No int i = 0;, não acho que alguém usaria int i{0}lá, e pode ser confuso (também, 0é tipo if int, então não haveria estreitamento ). Para todo o resto, seguiria o conselho de Juancho: prefira {}, cuidado com os poucos casos em que não o deve fazer. Observe que não há muitos tipos que aceitarão listas de inicializadores como argumentos de construtor, você pode esperar que contêineres e tipos semelhantes a contêiner (tupla ...) os tenham, mas a maioria do código chamará o construtor apropriado.
David Rodríguez - dribeas
3
@ user1304032 depende se você se preocupa com o estreitamento. Sim, então prefiro que o compilador me diga que int i{some floating point}é um erro, em vez de truncar silenciosamente.
juanchopanza
3
Com relação a "preferir {}, tome cuidado com os poucos casos em que você não deve": digamos que duas classes tenham um construtor semanticamente equivalente, mas uma classe também tenha uma lista de inicializadores. Os dois construtores equivalentes devem ser chamados de maneiras diferentes?
helami
3
@helami: "Digamos que duas classes tenham um construtor semanticamente equivalente, mas uma classe também tenha uma lista de inicializadores. Os dois construtores equivalentes deveriam ser chamados de maneiras diferentes?" Digamos que eu encontre a análise mais incômoda; isso pode acontecer em qualquer construtor para qualquer instância. É muito mais fácil evitar isso se você usar apenas {}para significar "inicializar", a menos que seja absolutamente impossível .
Nicol Bolas,
16

Fora do código genérico (ou seja, modelos), você pode (e eu faço) usar colchetes em qualquer lugar . Uma vantagem é que funciona em qualquer lugar, por exemplo, até mesmo para inicialização em classe:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

ou para argumentos de função:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

Para variáveis ​​que não presto muita atenção entre os estilos T t = { init };ou T t { init };, acho que a diferença é pequena e, na pior das hipóteses, só resultará em uma mensagem útil do compilador sobre o uso incorreto de um explicitconstrutor.

Para tipos que aceitam, std::initializer_listembora às vezes, obviamente, os não- std::initializer_listconstrutores são necessários (sendo o exemplo clássico std::vector<int> twenty_answers(20, 42);). Não há problema em não usar aparelho ortodôntico.


Quando se trata de código genérico (ou seja, em modelos), o último parágrafo deve ter gerado alguns avisos. Considere o seguinte:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Em seguida, auto p = make_unique<std::vector<T>>(20, T {});cria um vetor de tamanho 2 se Tfor eg int, ou um vetor de tamanho 20 se Tfor std::string. Um sinal muito revelador de que há algo muito errado acontecendo aqui é que não há nenhum traço que pode salvá-lo aqui (por exemplo, com SFINAE): std::is_constructibleé em termos de inicialização direta, enquanto estamos usando a inicialização de chave que adia para dirigir inicialização se e somente se não houver nenhum construtor std::initializer_listinterferindo. Da mesma forma std::is_convertiblenão ajuda em nada.

Eu investiguei se é de fato possível rolar à mão uma característica que pode consertar isso, mas não estou muito otimista sobre isso. Em todo caso, não acho que estaríamos perdendo muito, acho que o fato de make_unique<T>(foo, bar)resultar em uma construção equivalente a T(foo, bar)é muito intuitivo; especialmente porque make_unique<T>({ foo, bar })é bastante diferente e só faz sentido se fooe bartiver o mesmo tipo.

Portanto, para código genérico, eu só uso chaves para inicialização de valor (por exemplo, T t {};ou T t = {};), o que é muito conveniente e acho superior ao método C ++ 03 T t = T();. Caso contrário, é a sintaxe de inicialização direta (ou seja T t(a0, a1, a2);), ou às vezes a construção padrão ( T t; stream >> t;sendo o único caso em que eu uso isso, eu acho).

Isso não significa que todas as chaves sejam ruins, considere o exemplo anterior com correções:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

Isso ainda usa colchetes para construir o std::unique_ptr<T>, embora o tipo real dependa do parâmetro do modelo T.

Luc Danton
fonte
@interjay Alguns dos meus exemplos podem realmente precisar usar tipos não assinados, por exemplo, make_unique<T>(20u, T {})para Tser unsignedou std::string. Não tenho muita certeza dos detalhes. (Observe que eu também comentei sobre as expectativas em relação à inicialização direta vs inicialização de chaves, por exemplo, funções de encaminhamento perfeito.) std::string c("qux");Não foi especificado para funcionar como uma inicialização em classe para evitar ambigüidades com declarações de função de membro na gramática.
Luc Danton,
@interjay Não concordo com você no primeiro ponto, fique à vontade para verificar 8.5.4 Inicialização de lista e 13.3.1.7 Inicialização por inicialização de lista. Quanto ao segundo, você precisa dar uma olhada mais de perto no que escrevi (que se refere à inicialização em classe ) e / ou a gramática C ++ (por exemplo , declarador de membro , que faz referência a inicializador de chave ou igual ).
Luc Danton,
Hmm, você está certo - eu estava testando com o GCC 4.5 anteriormente, o que parecia confirmar o que eu estava dizendo, mas o GCC 4.6 concorda com você. E eu perdi o fato de que você estava falando sobre inicialização em classe. Me desculpe.
intervalo de