A inicialização uniforme do C ++ 11 é um substituto para a sintaxe do estilo antigo?

172

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.

void.pointer
fonte
Esse pode ser um assunto melhor discutido no Programmers.se. Parece inclinar-se para o lado bom subjetivo.
Nicol Bolas
6
@ NicolBolas: Por outro lado, sua excelente resposta pode ser um candidato muito bom para a tag c ++ - faq. Acho que não tivemos uma explicação para isso postada antes.
Matthieu M.

Respostas:

233

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 vec3duas 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::stringsabe 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::stringconstrutor 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 autode 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 foovalor 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 vectoralgo 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 vectortipo e, portanto, o compilador estaria livre para escolher os outros construtores.

Nicol Bolas
fonte
11
+1 Pregado. Estou excluindo minha resposta, já que a sua aborda todos os mesmos pontos com muito mais detalhes.
R. Martinho Fernandes
21
O último ponto é por que eu realmente gostaria std::vector<int> v{100, std::reserve_tag};. Da mesma forma com std::resize_tag. Atualmente, são necessárias duas etapas para reservar espaço vetorial.
Xeo
6
@NicolBolas - Dois pontos: eu pensei que o problema com a análise irritante era foo (), não Bar (). Em outras palavras, se você o fizesse 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?
void.pointer
7
@RobertDailey: "se você o fizesse 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 que Bar()poderia ser um nome de tipo ou um valor temporário. É isso que cria a ambiguidade do compilador.
Nicol Bolas
8
unpleasant behavior- existe um novo termo padrão a ser lembrado:>
sehe
64

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:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

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:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

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:

void DoSomething(const CoolStringType& str);

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:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Exemplo do uso da noção # 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Acho ruim que o novo padrão permita que as pessoas inicializem escadas assim:

Stairs s {2, 4, 6};

... 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 é:

T var1;
T var2 (var1);

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:

T var2 {var1}; // fails if T is std::array for example
TommiT
fonte
48
Se você tiver "<lotes e lotes de código aqui>", será difícil entender seu código, independentemente da sintaxe.
Kevin cline
8
Além da IMO, é dever do seu IDE informá-lo que tipo está retornando (por exemplo, passando o mouse). Claro, se você não usar um IDE, você tomou o fardo sobre si mesmo :)
abergmeier
4
@ TommiT Eu concordo com algumas partes do que você diz. No entanto, no mesmo espírito que o debate da declaração de tipoauto 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).
sehe
5
" Mas a sintaxe usando chaves não funciona se o tipo T for um agregado: " Observe que este é um defeito relatado no comportamento esperado padrão, e não intencional.
Nicol Bolas
5
"Agora, ele terá que rolar de volta para a assinatura da função" se você precisar rolar, sua função é muito grande.
Roteamento de milhas
-3

Se seus construtores merely copy their parametersnas respectivas variáveis ​​de classe in exactly the same ordernas 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
2
Por que você diz que pode ser mais rápido?
Jbcoe # 26/16
Isto está incorreto. Não há nenhuma exigência para declarar um construtor: 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.
Tim