Por que o C ++ 11 não oferece suporte a listas de inicializadores designadas como C99? [fechadas]

121

Considerar:

struct Person
{
    int height;
    int weight;
    int age;
};

int main()
{
    Person p { .age = 18 };
}

O código acima é legal no C99, mas não é legal no C ++ 11.

Como era o a justificativa do comitê padrão para excluir o suporte para um recurso tão útil?

xmllmx
fonte
10
Aparentemente, não fazia sentido para o comitê de design incluí-lo ou simplesmente não aparecia nas reuniões. É importante notar que os inicializadores designados do C99 não estão em nenhuma das versões de especificação do C ++. Construtores parecem ser a construção de inicialização preferida, e por um bom motivo: eles garantem uma inicialização de objeto consistente, se você os escrever corretamente.
Robert Harvey,
19
Seu raciocínio é retrógrado, uma linguagem não precisa ter uma razão para não ter uma característica, ela precisa de uma razão para ter uma e uma forte. C ++ já está inchado o suficiente, como está.
Matthieu M.
42
Uma boa razão (que não pode ser resolvida com construtores, exceto escrevendo wrappers estupefacientes) é que, quer você use C ++ ou não, a maioria das APIs reais são C, não C ++, e não poucas delas fazem com que você forneça uma estrutura na qual deseja definir um ou dois campos - e não necessariamente o primeiro - mas precisa ter o resto inicializado com zero. A API Win32 OVERLAPPEDé um exemplo. Ser capaz de escrever ={.Offset=12345};tornaria o código muito mais claro (e provavelmente menos sujeito a erros). Os soquetes BSD são um exemplo semelhante.
Damon
14
O código em mainC99 não é legal. Deve ler struct Person p = { .age = 18 };
chqrlie
14
FYI C ++ 20 suportará inicializadores designados
Andrew Tomazos

Respostas:

34

C ++ possui construtores. Se fizer sentido inicializar apenas um membro, isso pode ser expresso no programa, implementando um construtor apropriado. Esse é o tipo de abstração que o C ++ promove.

Por outro lado, o recurso de inicializadores designados é mais sobre expor e tornar os membros fáceis de acessar diretamente no código do cliente. Isso leva a coisas como ter uma pessoa de 18 (anos?), Mas com altura e peso zero.


Em outras palavras, os inicializadores designados oferecem suporte a um estilo de programação em que os internos são expostos e o cliente tem flexibilidade para decidir como deseja usar o tipo.

Em vez disso, C ++ está mais interessado em colocar a flexibilidade do lado do designer de um tipo, para que os designers possam facilitar o uso correto de um tipo e dificultar o uso incorreto. Colocar o designer no controle de como um tipo pode ser inicializado faz parte disso: o designer determina construtores, inicializadores na classe, etc.

bames53
fonte
12
Por favor, mostre um link de referência para o que você diz ser a razão para o C ++ não ter inicializadores designados. Não me lembro de ter visto a proposta para isso.
Johannes Schaub - litb
20
Não é a própria razão de não fornecer um construtor para Personque seu autor deseje fornecer a maior flexibilidade possível para os usuários definirem e inicializarem os membros? O usuário também já pode escrever Person p = { 0, 0, 18 };(e por boas razões).
Johannes Schaub - litb
7
Algo semelhante foi recentemente aceito na especificação C ++ 14 por open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3605.html .
Johannes Schaub - litb
4
@ JohannesSchaub-litb Não estou falando sobre a causa imediata e puramente mecânica (isto é, não foi proposta ao comitê). Estou descrevendo o que acredito ser o fator dominante. - Persontem um design muito C, então os recursos C podem fazer sentido. No entanto, C ++ provavelmente permite um design melhor que também elimina a necessidade de inicializadores designados. - Em minha opinião, remover a restrição de inicializadores de classe para agregados está muito mais de acordo com o ethos de C ++ do que inicializadores designados.
bames53
4
A substituição C ++ para isso pode ser chamada de argumentos de função. Mas a partir de agora, argumentos de nome não existem oficialmente. Veja N4172 Argumentos nomeados para uma proposta deste. Isso tornaria o código menos sujeito a erros e mais fácil de ler.
David Baird
89

Em 15 de julho de 17, P0329R4 foi aceito nopadrão: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Isso traz suporte limitado paraInicializadores designados de. Esta limitação é descrita a seguir por C.1.7 [diff.decl] .4, dado:

struct A { int x, y; };
struct B { struct A a; };

As seguintes inicializações designadas, que são válidas em C, são restritas em C ++:

  • struct A a = { .y = 1, .x = 2 } é inválido em C ++ porque os designadores devem aparecer na ordem de declaração dos membros de dados
  • int arr[3] = { [1] = 5 } é inválido em C ++ porque a inicialização designada por matriz não é compatível
  • struct B b = {.a.x = 0} é inválido em C ++ porque os designadores não podem ser aninhados
  • struct A c = {.x = 1, 2} é inválido em C ++ porque todos ou nenhum dos membros de dados devem ser inicializados por designadores

Para e Boost anterior, na verdade, tem suporte para inicializadores designados e houve inúmeras propostas para adicionar suporte aopadrão, por exemplo: n4172 e a proposta de Daryle Walker para adicionar designação aos inicializadores . As propostas citam a implementação deInicializadores designados da em Visual C ++, gcc e Clang, afirmando:

Acreditamos que as mudanças serão relativamente simples de implementar

Mas o comitê padrão rejeita repetidamente tais propostas , afirmando:

O EWG encontrou vários problemas com a abordagem proposta e não achou viável tentar resolver o problema, pois já foi tentado várias vezes e sempre falhou

Os comentários de Ben Voigt me ajudaram a ver os problemas intransponíveis dessa abordagem; dado:

struct X {
    int c;
    char a;
    float b;
};

Em que ordem essas funções seriam chamadas em : struct X foo = {.a = (char)f(), .b = g(), .c = h()}? Surpreendentemente, em:

A ordem de avaliação das subexpressões em qualquer inicializador é sequenciada indeterminadamente [ 1 ]

(Visual C ++, gcc e Clang parecem ter um comportamento acordado, pois todos farão as chamadas nesta ordem :)

  1. h()
  2. f()
  3. g()

Mas a natureza indeterminada do padrão significa que, se essas funções tivessem qualquer interação, o estado do programa resultante também seria indeterminado, e o compilador não o avisaria : Existe uma maneira de ser avisado sobre o mau comportamento de inicializadores designados?

não têm requisitos de lista de inicializador rigorosas 11.6.4 [dcl.init.list] 4:

Na lista de inicializadores de uma lista de inicializações com chaves, as cláusulas inicializadoras, incluindo qualquer uma que resulte de expansões de pacote (17.5.3), são avaliadas na ordem em que aparecem. Ou seja, cada cálculo de valor e efeito colateral associado a uma determinada cláusula inicializadora é sequenciado antes de cada cálculo de valor e efeito colateral associado a qualquer cláusula inicializadora que o segue na lista separada por vírgulas da lista inicializadora.

assim o suporte teria exigido que fosse executado na ordem:

  1. f()
  2. g()
  3. h()

Quebrando a compatibilidade com o anterior implementações.
Conforme discutido acima, este problema foi contornado pelas limitações dos inicializadores designados aceitos em. Eles fornecem um comportamento padronizado, garantindo a ordem de execução dos Inicializadores Designados.

Jonathan Mee
fonte
3
Claro, neste código: struct X { int c; char a; float b; }; X x = { .a = f(), .b = g(), .c = h() };a chamada para h()é realizada antes de f()ou g(). Se a definição de struct Xnão estiver próxima, isso será muito surpreendente. Lembre-se de que as expressões do inicializador não precisam ser livres de efeitos colaterais.
Ben Voigt
2
Claro, isso não é nada novo, a inicialização do ctor member já tem esse problema, mas está na definição de um membro da classe, então o acoplamento estreito não é surpresa. E os inicializadores designados não podem referenciar os outros membros da mesma forma que os inicializadores de membros ctor podem.
Ben Voigt
2
@MattMcNabb: Não, não é mais extremo. Mas espera-se que o desenvolvedor que implementa o construtor de classe conheça a ordem de declaração do membro. Considerando que o consumidor da classe pode ser um programador totalmente diferente. Visto que o objetivo é permitir a inicialização sem ter que procurar a ordem dos membros, isso parece uma falha fatal na proposta. Como os inicializadores designados não podem fazer referência ao objeto que está sendo construído, a primeira impressão é que as expressões de inicialização podem ser avaliadas primeiro, na ordem de designação, depois a inicialização do membro na ordem de declaração. Mas ...
Ben Voigt
2
@JonathanMee: Bem, a outra pergunta respondeu que ... os inicializadores agregados C99 não estão ordenados, então não há expectativa de que os inicializadores designados sejam ordenados. As listas de inicialização com chaves C ++ SÃO ordenadas, e a proposta para inicializadores designados usa uma ordem potencialmente surpreendente (você não pode ser consistente tanto com a ordem lexical, usada para todas as listas de init com chaves, quanto com a ordem de membro, usada para inicializador de ctor -lists)
Ben Voigt
3
Jonathan: "o suporte a c ++ teria exigido que fosse executado na ordem [...] Quebrando a compatibilidade com as implementações anteriores de c99." Eu não entendo este, desculpe. 1. Se o pedido for indeterminado em C99, então obviamente qualquer pedido real deve ser adequado, incluindo qualquer escolha arbitrária de C ++. b) Não suportando o des. inicializadores em tudo meio que já quebram a compatibilidade com C99 ainda mais ...
Sz.
34

Um pouco de hackeagem, então apenas compartilhar por diversão.

#define with(T, ...)\
    ([&]{ T ${}; __VA_ARGS__; return $; }())

E use-o como:

MyFunction(with(Params,
    $.Name = "Foo Bar",
    $.Age  = 18
));

que se expande para:

MyFunction(([&] {
 Params ${};
 $.Name = "Foo Bar", $.Age = 18;
 return $;
}()));
Keebus
fonte
Neat, cria um lambda com uma variável chamada $de tipo Te você atribui seus membros diretamente antes de retorná-lo. Nifty. Eu me pergunto se há alguma preocupação de desempenho com ele.
TankorSmash
1
Em uma construção otimizada, você não vê vestígios do lambda nem de sua invocação. Está tudo embutido.
keebus de
1
Eu absolutamente amo essa resposta.
Seph Reed
6
Uau. Nem sabia que $ era um nome válido.
Chris Watts,
Era suportado por compiladores C legados e o suporte permaneceu para compatibilidade com versões anteriores.
Keebus
22

O inicializador designado está atualmente incluído no corpo de trabalho do C ++ 20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf, então podemos finalmente vê-los!

SergeyA
fonte
3
Mas observe que eles são restritos: em C ++, o suporte de inicialização designado é restrito em comparação com a funcionalidade correspondente em C. Em C ++, designadores para membros de dados não estáticos devem ser especificados na ordem de declaração, designadores para elementos de array e designadores aninhados não são os inicializadores suportados e designados e não designados não podem ser misturados na mesma lista de inicializadores. Isso significa que, em particular, você ainda não será capaz de criar facilmente uma tabela de pesquisa com chave enum .
Ruslan
@Ruslan: Eu me pergunto por que o C ++ os restringiu tanto? Eu entendo que pode haver confusão sobre se a ordem em que os valores dos itens são avaliados e / ou gravados na estrutura corresponde à ordem em que os itens são especificados na lista de inicialização ou a ordem em que os membros aparecem na estrutura, mas o A solução para isso seria simplesmente dizer que as expressões de inicialização são executadas em uma sequência arbitrária e o tempo de vida do objeto não começa até que a inicialização seja concluída (o &operador retornaria o endereço que o objeto terá durante seu tempo de vida).
supercat de
5

Dois recursos centrais do C99 que faltam no C ++ 11 mencionam “Inicializadores designados e C ++”.

Acho que o 'inicializador designado' está relacionado com a otimização potencial. Aqui eu uso “gcc / g ++” 5.1 como exemplo.

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>    
struct point {
    int x;
    int y;
};
const struct point a_point = {.x = 0, .y = 0};
int foo() {
    if(a_point.x == 0){
        printf("x == 0");
        return 0;
    }else{
        printf("x == 1");
        return 1;
    }
}
int main(int argc, char *argv[])
{
    return foo();
}

Sabíamos em tempo de compilação, a_point.xé zero, então poderíamos esperar que foofosse otimizado em um único printf.

$ gcc -O3 a.c
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function foo:
   0x00000000004004f0 <+0>: sub    $0x8,%rsp
   0x00000000004004f4 <+4>: mov    $0x4005bc,%edi
   0x00000000004004f9 <+9>: xor    %eax,%eax
   0x00000000004004fb <+11>:    callq  0x4003a0 <printf@plt>
   0x0000000000400500 <+16>:    xor    %eax,%eax
   0x0000000000400502 <+18>:    add    $0x8,%rsp
   0x0000000000400506 <+22>:    retq   
End of assembler dump.
(gdb) x /s 0x4005bc
0x4005bc:   "x == 0"

fooé otimizado para imprimir x == 0apenas.

Para a versão C ++,

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
struct point {
    point(int _x,int _y):x(_x),y(_y){}
    int x;
    int y;
};
const struct point a_point(0,0);
int foo() {
    if(a_point.x == 0){
        printf("x == 0");
        return 0;
    }else{
        printf("x == 1");
        return 1;
    }
}
int main(int argc, char *argv[])
{
    return foo();
}

E esta é a saída do código de montagem otimizado.

g++ -O3 a.cc
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function _Z3foov:
0x00000000004005c0 <+0>:    push   %rbx
0x00000000004005c1 <+1>:    mov    0x200489(%rip),%ebx        # 0x600a50 <_ZL7a_point>
0x00000000004005c7 <+7>:    test   %ebx,%ebx
0x00000000004005c9 <+9>:    je     0x4005e0 <_Z3foov+32>
0x00000000004005cb <+11>:   mov    $0x1,%ebx
0x00000000004005d0 <+16>:   mov    $0x4006a3,%edi
0x00000000004005d5 <+21>:   xor    %eax,%eax
0x00000000004005d7 <+23>:   callq  0x400460 <printf@plt>
0x00000000004005dc <+28>:   mov    %ebx,%eax
0x00000000004005de <+30>:   pop    %rbx
0x00000000004005df <+31>:   retq   
0x00000000004005e0 <+32>:   mov    $0x40069c,%edi
0x00000000004005e5 <+37>:   xor    %eax,%eax
0x00000000004005e7 <+39>:   callq  0x400460 <printf@plt>
0x00000000004005ec <+44>:   mov    %ebx,%eax
0x00000000004005ee <+46>:   pop    %rbx
0x00000000004005ef <+47>:   retq   

Podemos ver que a_pointnão é realmente um valor constante de tempo de compilação.

wcy
fonte
8
Agora, por favor, tente constexpr point(int _x,int _y):x(_x),y(_y){}. O otimizador do clang ++ parece eliminar a comparação em seu código também. Portanto, este é apenas um problema de QoI.
dyp
Eu também esperaria que todo o objeto a_point fosse otimizado se tivesse uma ligação interna. ou seja, coloque-o no namespace anônimo e veja o que acontece. goo.gl/wNL0HC
Arvid
@dyp: Mesmo apenas definir um construtor só é possível se o tipo estiver sob seu controle. Você não pode fazer isso, por exemplo, para struct addrinfoou struct sockaddr_in, então você fica com atribuições separadas de declarações.
musiphil
2
@musiphil Pelo menos no C ++ 14, essas estruturas de estilo C podem ser configuradas corretamente em uma função constexpr como variáveis ​​locais usando atribuição e, em seguida, retornadas dessa função. Além disso, meu objetivo não foi mostrar uma implementação alternativa do construtor em C ++ que permite a otimização, mas mostrar que é possível para o compilador realizar essa otimização se a forma de inicialização for diferente. Se o compilador é "bom o suficiente" (ou seja, suporta esta forma de otimização), então deve ser irrelevante se você usa um ctor ou inicializadores designados, ou qualquer outra coisa.
dyp