Eu entendo que a inicialização uniforme do C ++ 11 resolve alguma ambiguidade sintática na linguagem, mas em muitas apresentações de Bjarne Stroustrup (particularmente aquelas durante as palestras do GoingNative 2012), seus exemplos usam principalmente essa sintaxe agora sempre que ele está construindo objetos.
É recomendado agora usar inicialização uniforme em todos os casos? Qual deve ser a abordagem geral para esse novo recurso no que diz respeito ao estilo de codificação e ao uso geral? Quais são alguns motivos para não usá-lo?
Observe que, na minha opinião, estou pensando principalmente na construção de objetos como meu caso de uso, mas se houver outros cenários a serem considerados, entre em contato.
Respostas:
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:
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:Tudo funciona.
Melhor ainda é para argumentos de função. Considere isto:
Isso funciona sem ter que digitar um nome de tipo, porque
std::string
sabe como construir-se a partir de umconst 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. Ostd::string
construtor que usa aconst char*
terá que aceitar o comprimento da string se eu passar apenas aconst 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 comauto
, podemos evitar nomes de tipos extras: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émauto v = GetSomething();
requer uma análise da definição deGetSomething
. Mas isso não deixouauto
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.
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 umaBar
, eofoo
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 chamadoBar
, o que significa que está criando um temporário. Ou pode ser uma função que não aceita parâmetros e retorna aBar
.A inicialização uniforme não pode ser interpretada como um protótipo de função:
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 ++. ComTypename{}
, 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.
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:
O que isso faz? Poderia criar um
vector<int>
com cem itens construídos por padrão. Ou poderia criar umvector<int>
item com 1 com valor100
. 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 aceitainitializer_list<int>
e {100} poderia ser um válidoinitializer_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 dessevector
tipo e, portanto, o compilador estaria livre para escolher os outros construtores.fonte
std::vector<int> v{100, std::reserve_tag};
. Da mesma forma comstd::resize_tag
. Atualmente, são necessárias duas etapas para reservar espaço vetorial.int foo(10)
, não teria o mesmo problema? Segundo, outro motivo para não usá-lo parece ser mais um problema de excesso de engenharia, mas e se construíssemos todos os nossos objetos usando{}
, mas um dia depois, adicionei um construtor para as listas de inicialização? Agora todas as minhas instruções de construção se transformam em instruções da lista de inicializadores. Parece muito frágil em termos de refatoração. Algum comentário sobre isso?int foo(10)
, não teria o mesmo problema?" O número 10 é um literal inteiro e um literal inteiro nunca pode ser um nome de tipo. A análise irritante vem do fato de queBar()
poderia ser um nome de tipo ou um valor temporário. É isso que cria a ambiguidade do compilador.unpleasant behavior
- existe um novo termo padrão a ser lembrado:>Discordo da seção de respostas de Nicol Bolas, minimiza nomes de tipos redundantes . Como o código é escrito uma vez e lido várias vezes, devemos tentar minimizar a quantidade de tempo que leva para ler e entender o código, e não a quantidade de tempo que leva para escrever o código. Tentar simplesmente minimizar a digitação está tentando otimizar a coisa errada.
Veja o seguinte código:
Alguém que lê o código acima pela primeira vez provavelmente não entenderá a declaração de retorno imediatamente, porque quando chegar a essa linha, ele terá esquecido o tipo de retorno. Agora, ele precisará rolar de volta para a assinatura da função ou usar algum recurso IDE para ver o tipo de retorno e entender completamente a instrução de retorno.
E aqui novamente, não é fácil para alguém que lê o código pela primeira vez entender o que está realmente sendo construído:
O código acima será interrompido quando alguém decidir que DoSomething também deve oferecer suporte a algum outro tipo de string e adiciona essa sobrecarga:
Se o CoolStringType tiver um construtor que use um const char * e um size_t (assim como std :: string), a chamada para DoSomething ({strValue, strLen}) resultará em um erro de ambiguidade.
Minha resposta para a pergunta real:
Não, a Inicialização uniforme não deve ser pensada como um substituto para a sintaxe do construtor de estilo antigo.
E meu raciocínio é o seguinte:
se duas afirmações não têm o mesmo tipo de intenção, elas não devem ter a mesma aparência. Existem dois tipos de noções de inicialização de objeto:
1) Pegue todos esses itens e coloque-os neste objeto que estou inicializando.
2) Construa esse objeto usando esses argumentos que forneci como guia.
Exemplos do uso da noção # 1:
Exemplo do uso da noção # 2:
Acho ruim que o novo padrão permita que as pessoas inicializem escadas assim:
... porque isso ofusca o significado do construtor. Uma inicialização assim se parece com a noção # 1, mas não é. Não está despejando três valores diferentes de alturas de etapas nos objetos s, mesmo que pareça estar. E também, mais importante, se uma implementação de biblioteca do Stairs como acima foi publicada e os programadores o estiver usando, e se o implementador da biblioteca posteriormente adicionar um construtor initializer_list ao Stairs, todo o código que estiver usando o Stairs with Uniform Initialization Sintaxe vai quebrar.
Eu acho que a comunidade C ++ deve concordar com uma convenção comum sobre como a Inicialização Uniforme é usada, ou seja, uniformemente em todas as inicializações ou, como eu sugiro fortemente, separar essas duas noções de inicialização e, assim, esclarecer a intenção do programador para o leitor de o código.
DEPOIS:
Aqui está outro motivo pelo qual você não deve considerar a Inicialização uniforme como um substituto para a sintaxe antiga e por que não pode usar a notação entre chaves para todas as inicializações:
Digamos, sua sintaxe preferida para fazer uma cópia é:
Agora você acha que deve substituir todas as inicializações pela nova sintaxe de colchete para que você possa (e o código parecerá) mais consistente. Mas a sintaxe usando chaves não funcionará se o tipo T for um agregado:
fonte
auto
vs. explícito , eu argumentaria por um equilíbrio: inicializadores uniformes são bastante importantes em situações de meta-programação de modelos em que o tipo geralmente é bastante óbvio de qualquer maneira. Isso evitará repetir o seu danado complicado-> decltype(....)
para encantamento, por exemplo, para modelos simples de funções on-line (me fez chorar).Se seus construtores
merely copy their parameters
nas respectivas variáveis de classein exactly the same order
nas quais são declarados dentro da classe, o uso de inicialização uniforme pode eventualmente ser mais rápido (mas também pode ser absolutamente idêntico) do que chamar o construtor.Obviamente, isso não muda o fato de que você sempre deve declarar o construtor.
fonte
struct X { int i; }; int main() { X x{42}; }
. Também está incorreto que a inicialização uniforme possa ser mais rápida que a inicialização de valor.