Por que o C ++ 11 não suporta listas de inicializadores designadas como C99?

Considerar:

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

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

Qual é a razão pela qual o C ++ 11 não suporta um recurso tão prático?

C ++ tem 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 designado tem mais a ver com a exposição e facilitar o access dos membros diretamente ao código do cliente. Isso leva a coisas como ter uma pessoa de 18 anos (anos?), Mas com altura e peso de zero.


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

C ++ está mais interessado em colocar a flexibilidade do lado do designer de um tipo, então os designers podem facilitar o uso correto de um tipo e a dificuldade de uso incorreto. Colocar o designer no controle de como um tipo pode ser inicializado é parte disso: o designer determina construtores, inicializadores em class, etc.

Em 15 de julho de 17, o P0329R4 foi aceito no padrão c ++ 20 : http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Isso traz suporte limitado para os inicializadores designados pelo c99 . Esta limitação é descrita da seguinte forma 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 boot designada da matriz não é suportada
  • struct B b = {.ax = 0} é inválido em C ++ porque os designadores não podem ser nesteds
  • struct A c = {.x = 1, 2} é inválido em C ++ porque todos ou nenhum dos membros de dados devem ser inicializados por designadores

Para o c ++ 17 e o anterior, o Boost realmente tem suporte para Intializers Designados e tem havido numerosas propostas para adicionar suporte ao padrão c ++ , por exemplo: n4172 e a Proposta de Daryle Walker para Adicionar Designação aos Inicializadores . As propostas citam a implementação dos Initializers Designados do c99 no Visual C ++, gcc e Clang, alegando:

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

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

O EWG encontrou vários problemas com a abordagem proposta e não achou viável tentar solucionar o problema, pois ele foi tentado muitas vezes e sempre que falhava

Os comentários de Ben Voigt me ajudaram a ver os problemas insuperáveis ​​com essa abordagem; dado:

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

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

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 eles 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 alguma 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 inicializadores designados com comportamento incorreto?

c ++ tem requisitos estritos de lista de inicializadores 11.6.4 [dcl.init.list] 4:

Dentro da lista de inicializadores de uma lista init-braced, as cláusulas de inicializador, incluindo qualquer resultado de expansões de pacotes (17.5.3), são avaliadas na ordem em que aparecem. Ou seja, todo cálculo de valor e efeito colateral associados a uma determinada cláusula inicializadora são sequenciados antes de cada cálculo de valor e efeito colateral associado a qualquer cláusula inicializadora que o segue na lista separada por vírgula da lista de inicializadores.

Então, o suporte c ++ teria exigido que isso fosse executado na ordem:

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

Rompendo a compatibilidade com implementações anteriores do c99 . O que é necessário aqui é um comportamento explícito que rege a ordem de execução dos inicializadores designados.

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, para que possamos finalmente vê-los!

Um pouco de hackery, então apenas compartilhando por diversão.

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

E usá-lo como:

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

que se expande para:

 MyFunction(([&] { Params ${}; $.Name = "Foo Bar", $.Age = 18; return $; }())); 

Dois resources principais do C99 que o C ++ 11 não menciona “Initializers designados e C ++”.

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

 #include  #include  #include  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 que em compilation time, a_point.x é zero, então poderíamos esperar que foo fosse otimizado em um único printf .

 $ gcc -O3 ac $ 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  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 == 0 apenas.

Para a versão C ++,

 #include  #include  #include  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 assembly 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  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  0x00000000004005ec < +44>: mov %ebx,%eax 0x00000000004005ee < +46>: pop %rbx 0x00000000004005ef < +47>: retq 

Podemos ver que a_point não é realmente um valor constante de tempo de compilation.