¿Por qué C ++ 11 no admite listas de inicializadores designados como C99? [cerrado]

121

Considerar:

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

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

El código anterior es legal en C99, pero no es legal en C ++ 11.

Cual fue el ¿Cuál es el fundamento del comité estándar para excluir el soporte para una característica tan útil?

xmllmx
fuente
10
Al parecer, no tenía sentido para el comité de diseño incluirlo, o simplemente no surgió en las reuniones. Vale la pena señalar que los inicializadores designados C99 no están en ninguna de las versiones de la especificación C ++. Los constructores parecen ser la construcción de inicialización preferida, y por una buena razón: garantizan una inicialización de objetos coherente, si los escribe correctamente.
Robert Harvey
19
Su razonamiento es al revés, un lenguaje no necesita tener una razón para no tener una característica, necesita una razón para tener una y una fuerte en eso. C ++ está lo suficientemente inflado, tal como está.
Matthieu M.
42
Una buena razón (que no se puede resolver con constructores excepto escribiendo envoltorios asombrosos) es que, ya sea que use o no C ++, la mayoría de las API reales son C, no C ++, y no pocas de ellas le hacen proporcionar una estructura en la que desea establecer uno o dos campos, y no necesariamente el primero, pero es necesario que el resto se inicialice a cero. La API de Win32 OVERLAPPEDes un ejemplo. Ser capaz de escribir ={.Offset=12345};haría que el código fuera mucho más claro (y probablemente menos propenso a errores). Los enchufes BSD son un ejemplo similar.
Damon
14
El código en mainC99 no es legal. Debería leer struct Person p = { .age = 18 };
chqrlie
14
FYI C ++ 20 admitirá inicializadores designados
Andrew Tomazos

Respuestas:

34

C ++ tiene constructores. Si tiene sentido inicializar solo un miembro, entonces eso se puede expresar en el programa implementando un constructor apropiado. Este es el tipo de abstracción que promueve C ++.

Por otro lado, la función de inicializadores designados se trata más de exponer y facilitar el acceso de los miembros directamente en el código del cliente. Esto lleva a cosas como tener una persona de 18 años (¿años?) Pero con una altura y un peso de cero.


En otras palabras, los inicializadores designados admiten un estilo de programación en el que se exponen los componentes internos y el cliente tiene flexibilidad para decidir cómo desea utilizar el tipo.

C ++ está más interesado en poner la flexibilidad del lado del diseñador de un tipo, por lo que los diseñadores pueden facilitar el uso correcto de un tipo y dificultar el uso incorrecto. Poner al diseñador en control de cómo se puede inicializar un tipo es parte de esto: el diseñador determina los constructores, los inicializadores en clase, etc.

bames53
fuente
12
Muestre un enlace de referencia para lo que dice que es la razón por la que C ++ no tiene inicializadores designados. No recuerdo haber visto nunca la propuesta.
Johannes Schaub - litb
20
¿No es la misma razón de no proporcionar un constructor Personque su autor quisiera proporcionar la mayor flexibilidad posible para que los usuarios establezcan e inicialicen los miembros? El usuario también puede escribir Person p = { 0, 0, 18 };(y por buenas razones).
Johannes Schaub - litb
7
Algo similar ha sido aceptado recientemente en la especificación de C ++ 14 por open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3605.html .
Johannes Schaub - litb
4
@ JohannesSchaub-litb No estoy hablando de la causa próxima puramente mecánica (es decir, no se ha propuesto al comité). Estoy describiendo lo que creo que es el factor dominante. - Persontiene un diseño muy C, por lo que las funciones C pueden tener sentido. Sin embargo, C ++ probablemente permite un mejor diseño que también evita la necesidad de inicializadores designados. - En mi opinión, eliminar la restricción de los inicializadores de clase para los agregados está mucho más en línea con el espíritu de C ++ que los inicializadores designados.
bames53
4
El reemplazo de C ++ para esto podría denominarse argumentos de función. Pero a partir de ahora, los argumentos de nombre no existen oficialmente. Ver N4172 Argumentos nombrados para una propuesta de esto. Haría que el código fuera menos propenso a errores y más fácil de leer.
David Baird
89

El 15 de julio de 2017 P0329R4 fue aceptado en elestándar: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Esto brinda soporte limitado paraInicializadores designados. Esta limitación se describe como sigue en C.1.7 [diff.decl] .4, dado:

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

Las siguientes inicializaciones designadas, que son válidas en C, están restringidas en C ++:

  • struct A a = { .y = 1, .x = 2 } no es válido en C ++ porque los designadores deben aparecer en el orden de declaración de los miembros de datos
  • int arr[3] = { [1] = 5 } no es válido en C ++ porque la inicialización designada por matriz no es compatible
  • struct B b = {.a.x = 0} no es válido en C ++ porque los designadores no se pueden anidar
  • struct A c = {.x = 1, 2} no es válido en C ++ porque todos o ninguno de los miembros de datos deben ser inicializados por designadores

por y Boost anterior en realidad tiene soporte para Intializadores designados y ha habido numerosas propuestas para agregar soporte a laestándar, por ejemplo: n4172 y la propuesta de Daryle Walker para agregar una designación a los inicializadores . Las propuestas citan la implementación deInicializadores designados en Visual C ++, gcc y Clang que afirman:

Creemos que los cambios serán relativamente sencillos de implementar.

Pero el comité estándar rechaza repetidamente tales propuestas , afirmando:

EWG encontró varios problemas con el enfoque propuesto y no pensó que fuera factible intentar resolver el problema, ya que se ha intentado muchas veces y cada vez ha fallado.

Los comentarios de Ben Voigt me han ayudado a ver los problemas insuperables con este enfoque; dado:

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

¿En qué orden se llamarían estas funciones en : struct X foo = {.a = (char)f(), .b = g(), .c = h()}? Sorprendentemente, en:

El orden de evaluación de las subexpresiones en cualquier inicializador tiene una secuencia indeterminada [ 1 ]

(Visual C ++, gcc y Clang parecen tener un comportamiento acordado, ya que todos realizarán las llamadas en este orden :)

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

Pero la naturaleza indeterminada del estándar significa que si estas funciones tuvieran alguna interacción, el estado del programa resultante también sería indeterminado, y el compilador no le advertiría : ¿Hay alguna manera de recibir una advertencia sobre los inicializadores designados que se comportan mal?

no tienen requisitos estrictos de inicialización de listas 11.6.4 [dcl.init.list] 4:

Dentro de la lista de inicialización de una lista de inicialización entre llaves, las cláusulas de inicialización, incluidas las que resultan de las expansiones de paquetes (17.5.3), se evalúan en el orden en que aparecen. Es decir, cada cálculo de valor y efecto secundario asociado con una cláusula inicializadora dada se secuencia antes de cada cálculo de valor y efecto secundario asociado con cualquier cláusula inicializador que le siga en la lista separada por comas de la lista de inicializadores.

Entonces El soporte habría requerido que esto se ejecutara en el orden:

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

Rompiendo la compatibilidad con anteriores implementaciones.
Como se discutió anteriormente, este problema ha sido eludido por las limitaciones en Inicializadores Designados aceptados en. Proporcionan un comportamiento estandarizado, garantizando el orden de ejecución de los Inicializadores Designados.

Jonathan Mee
fuente
3
Claro, en este código: struct X { int c; char a; float b; }; X x = { .a = f(), .b = g(), .c = h() };la llamada a h()se realiza antes de f()o g(). Si la definición de struct Xno está cerca, esto será muy sorprendente. Recuerde que las expresiones de inicializador no tienen por qué estar libres de efectos secundarios.
Ben Voigt
2
Por supuesto, esto no es nada nuevo, la inicialización del miembro ctor ya tiene este problema, pero está en la definición de un miembro de clase, por lo que el acoplamiento estrecho no es una sorpresa. Y los inicializadores designados no pueden hacer referencia a los otros miembros de la forma en que lo hacen los inicializadores de miembros de ctor.
Ben Voigt
2
@MattMcNabb: No, no es más extremo. Pero se espera que el desarrollador que implementa el constructor de clases conozca el orden de declaración de los miembros. Mientras que el consumidor de la clase podría ser un programador completamente diferente. Dado que el objetivo es permitir la inicialización sin tener que buscar el orden de los miembros, esto parece un defecto fatal en la propuesta. Dado que los inicializadores designados no pueden hacer referencia al objeto que se está construyendo, la primera impresión es que las expresiones de inicialización podrían evaluarse primero, en orden de designación, luego la inicialización de miembros en orden de declaración. Pero ...
Ben Voigt
2
@JonathanMee: Bueno, la otra pregunta respondió que ... Los inicializadores agregados C99 están desordenados, por lo que no se espera que se ordenen los inicializadores designados. Las listas de inicialización con llaves de C ++ ESTÁN ordenadas, y la propuesta de inicializadores designados utiliza un orden potencialmente sorprendente (no puede ser coherente con el orden léxico, que se utiliza para todas las listas de inicialización con llaves, y el orden de los miembros, utilizado para ctor-initializer -listas)
Ben Voigt
3
Jonathan: "El soporte de c ++ habría requerido que esto se ejecutara en el orden [...] Rompiendo la compatibilidad con implementaciones anteriores de c99". No entiendo este, lo siento. 1. Si el orden es indeterminado en C99, entonces, obviamente, cualquier orden real debería estar bien, incluida cualquier elección arbitraria de C ++. b) No apoyar el des. Los inicializadores ya rompen un poco la compatibilidad con C99 aún más ...
Sz.
34

Un poco de piratería, así que solo comparte por diversión.

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

Y utilícelo como:

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

que se expande a:

MyFunction(([&] {
 Params ${};
 $.Name = "Foo Bar", $.Age = 18;
 return $;
}()));
Keebus
fuente
Neat, crea una lambda con una variable denominada $de tipo Ty usted asigna sus miembros directamente antes de devolverla. Hábil. Me pregunto si hay algún problema de rendimiento con él.
TankorSmash
1
En una compilación optimizada, no ve rastros de lambda ni su invocación. Todo está en línea.
keebus
1
Me encanta esta respuesta.
Seph Reed
6
Woah. Ni siquiera sabía que $ era un nombre válido.
Chris Watts
Fue compatible con compiladores de C heredados y el soporte se mantuvo por compatibilidad con versiones anteriores.
keebus
22

Los inicializadores designados se incluyen actualmente en el cuerpo de trabajo de C ++ 20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf ¡ así que finalmente podríamos verlos!

SergeyA
fuente
3
Pero tenga en cuenta que están restringidos: en C ++, el soporte de inicialización designado está restringido en comparación con la funcionalidad correspondiente en C.En C ++, los designadores para miembros de datos no estáticos deben especificarse en orden de declaración, los designadores para elementos de matriz y los designadores anidados no lo son admitidos, y los inicializadores designados y no designados no se pueden mezclar en la misma lista de inicializadores. Esto significa que, en particular, todavía no podrá crear fácilmente una tabla de búsqueda con clave de enumeración .
Ruslan
@Ruslan: Me pregunto por qué C ++ los restringió tanto. Entiendo que podría haber confusión acerca de si el orden en el que se evalúan y / o escriben los valores de los elementos en la estructura coincide con el orden en el que se especifican los elementos en la lista de inicialización, o el orden en el que aparecen los miembros en la estructura, pero el La solución a eso sería simplemente decir que las expresiones de inicialización se ejecutan en una secuencia arbitraria, y la vida útil del objeto no comienza hasta que se completa la inicialización (el &operador devolvería la dirección que tendrá el objeto durante su vida útil).
supercat
5

Dos características principales de C99 de las que C ++ 11 carece menciona "Inicializadores designados y C ++".

Creo que el 'inicializador designado' relacionado con la optimización potencial. Aquí utilizo “gcc / g ++” 5.1 como ejemplo.

#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 que en el momento de la compilación a_point.xes cero, por lo que podríamos esperar que fooesté optimizado en uno 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"

fooestá optimizado para imprimir x == 0únicamente.

Para la versión 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();
}

Y esta es la salida del código ensamblado optimizado.

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_pointrealmente no es un valor constante de tiempo de compilación.

wcy
fuente
8
Ahora inténtelo constexpr point(int _x,int _y):x(_x),y(_y){}. El optimizador de clang ++ parece eliminar la comparación en su código también. Entonces, esto es solo un problema de QoI.
dyp
También esperaría que todo el objeto a_point se optimizara si tuviera un enlace interno. es decir, colóquelo en el espacio de nombres anónimo y vea qué sucede. goo.gl/wNL0HC
Arvid
@dyp: Incluso definir un constructor es posible solo si el tipo está bajo su control. No puede hacer eso, por ejemplo, para struct addrinfoo struct sockaddr_in, por lo que le quedan asignaciones separadas de las declaraciones.
musiphil
2
@musiphil Al menos en C ++ 14, esas estructuras de estilo C se pueden configurar correctamente en una función constexpr como variables locales mediante la asignación, y luego se devuelven desde esa función. Además, mi punto no era mostrar una implementación alternativa del constructor en C ++ que permite la optimización, sino mostrar que es posible que el compilador realice esta optimización si la forma de inicialización es diferente. Si el compilador es "suficientemente bueno" (es decir, admite esta forma de optimización), entonces debería ser irrelevante si usa un ctor o inicializadores designados, o algo más.
dyp