O estilo de codificação é basicamente subjetivo e é altamente improvável que benefícios substanciais de desempenho venham dele. Mas aqui está o que eu diria que você ganha com o uso liberal de inicialização uniforme:
Minimiza nomes de tipos redundantes
Considere o seguinte:
vec3 GetValue()
{
return vec3(x, y, z);
}
Por que preciso digitar vec3
duas vezes? Existe um ponto para isso? O compilador sabe muito bem o que a função retorna. Por que não posso simplesmente dizer: "chame o construtor do que eu retorno com esses valores e retorne-o?" Com inicialização uniforme, eu posso:
vec3 GetValue()
{
return {x, y, z};
}
Tudo funciona.
Melhor ainda é para argumentos de função. Considere isto:
void DoSomething(const std::string &str);
DoSomething("A string.");
Isso funciona sem ter que digitar um nome de tipo, porque std::string
sabe como construir-se a partir de um const char*
implicitamente. Isso é ótimo. Mas e se essa sequência vier, digamos RapidXML. Ou uma string Lua. Ou seja, digamos que eu realmente saiba o comprimento da corda na frente. O std::string
construtor que usa a const char*
terá que aceitar o comprimento da string se eu passar apenas a const char*
.
Há uma sobrecarga que leva um comprimento explicitamente embora. Mas, para usá-lo, eu teria que fazer isso: DoSomething(std::string(strValue, strLen))
. Por que o typename extra está aí? O compilador sabe qual é o tipo. Assim como com auto
, podemos evitar nomes de tipos extras:
DoSomething({strValue, strLen});
Isso simplesmente funciona. Sem nomes de nomes, sem problemas, nada. O compilador faz seu trabalho, o código é mais curto e todos estão felizes.
É verdade que existem argumentos a serem feitos para que a primeira versão ( DoSomething(std::string(strValue, strLen))
) seja mais legível. Ou seja, é óbvio o que está acontecendo e quem está fazendo o que. Isso é verdade, até certo ponto; entender o código uniforme baseado em inicialização requer a observação do protótipo da função. Esta é a mesma razão pela qual alguns dizem que você nunca deve passar parâmetros por referência não const: para que você possa ver no site da chamada se um valor está sendo modificado.
Mas o mesmo poderia ser dito para auto
; saber o que você obtém auto v = GetSomething();
requer uma análise da definição de GetSomething
. Mas isso não deixou auto
de ser usado com um abandono quase imprudente quando você tem acesso a ele. Pessoalmente, acho que tudo ficará bem quando você se acostumar. Especialmente com um bom IDE.
Nunca obtenha a análise mais irritante
Aqui está um código.
class Bar;
void Func()
{
int foo(Bar());
}
Pop questionário: o que é foo
? Se você respondeu "uma variável", está errado. Na verdade, é o protótipo de uma função que recebe como parâmetro uma função que retorna uma Bar
, eo foo
valor de retorno da função é um int.
Isso é chamado de "Most Vexing Parse" do C ++ porque não faz absolutamente nenhum sentido para um ser humano. Infelizmente, as regras do C ++ exigem isso: se for possível interpretar como um protótipo de função, será . O problema é Bar()
; isso pode ser uma de duas coisas. Pode ser um tipo chamado Bar
, o que significa que está criando um temporário. Ou pode ser uma função que não aceita parâmetros e retorna a Bar
.
A inicialização uniforme não pode ser interpretada como um protótipo de função:
class Bar;
void Func()
{
int foo{Bar{}};
}
Bar{}
sempre cria um temporário. int foo{...}
sempre cria uma variável.
Existem muitos casos em que você deseja usar, Typename()
mas simplesmente não pode por causa das regras de análise do C ++. Com Typename{}
, não há ambiguidade.
Razões para não
O único poder real que você desiste é diminuir. Você não pode inicializar um valor menor com um valor maior com inicialização uniforme.
int val{5.2};
Isso não será compilado. Você pode fazer isso com a inicialização antiquada, mas não a inicialização uniforme.
Isso foi feito em parte para fazer com que as listas de inicializadores realmente funcionassem. Caso contrário, haveria muitos casos ambíguos com relação aos tipos de listas de inicializadores.
Certamente, alguns podem argumentar que esse código merece não ser compilado. Eu pessoalmente concordo; o estreitamento é muito perigoso e pode levar a um comportamento desagradável. Provavelmente, é melhor capturar esses problemas logo no estágio do compilador. No mínimo, o estreitamento sugere que alguém não está pensando muito sobre o código.
Observe que os compiladores geralmente avisam sobre esse tipo de coisa se o seu nível de aviso for alto. Realmente, tudo o que isso faz é transformar o aviso em erro forçado. Alguns podem dizer que você deveria fazer isso de qualquer maneira;)
Há um outro motivo para não:
std::vector<int> v{100};
O que isso faz? Poderia criar um vector<int>
com cem itens construídos por padrão. Ou poderia criar um vector<int>
item com 1 com valor 100
. Ambos são teoricamente possíveis.
Na realidade, ele faz o último.
Por quê? As listas de inicializadores usam a mesma sintaxe da inicialização uniforme. Portanto, deve haver algumas regras para explicar o que fazer no caso de ambiguidade. A regra é bastante simples: se o compilador puder usar um construtor de lista de inicializadores com uma lista entre chaves, então o fará . Como vector<int>
tem um construtor de lista de inicializadores que aceita initializer_list<int>
e {100} poderia ser um válido initializer_list<int>
, portanto, deve ser .
Para obter o construtor de dimensionamento, você deve usar em ()
vez de {}
.
Observe que se isso fosse vector
algo que não fosse conversível em um número inteiro, isso não aconteceria. Um initializer_list não se encaixaria no construtor da lista de inicializadores desse vector
tipo e, portanto, o compilador estaria livre para escolher os outros construtores.