C ++ 11 permite la inicialización en clase de miembros no estáticos y no const. ¿Qué cambió?

Antes de C ++ 11, solo podíamos realizar una inicialización en clase en miembros de const estáticos de tipo integral o de enumeración. Stroustrup discute esto en su C ++ FAQ , dando el siguiente ejemplo:

class Y { const int c3 = 7; // error: not static static int c4 = 7; // error: not const static const float c5 = 7; // error: not integral }; 

Y el siguiente razonamiento:

Entonces, ¿por qué existen estas restricciones inconvenientes? Por lo general, una clase se declara en un archivo de cabecera y, por lo general, un archivo de cabecera se incluye en muchas unidades de traducción. Sin embargo, para evitar reglas de enlazador complicadas, C ++ requiere que cada objeto tenga una definición única. Esa regla se rompería si C ++ permitiera la definición dentro de la clase de las entidades que debían almacenarse en la memoria como objetos.

Sin embargo, C ++ 11 relaja estas restricciones, permitiendo la inicialización en clase de miembros no estáticos (§12.6.2 / 8):

En un constructor no delegante, si un miembro de datos no estático determinado o clase base no está designado por un identificador-inicializador-memoria (incluido el caso donde no hay lista-inicializador-mem porque el constructor no tiene inicializador-ctor ) y la entidad no es una clase base virtual de una clase abstracta (10.4), entonces

  • si la entidad es un miembro de datos no estático que tiene un inicializador de llave o igual , la entidad se inicializa como se especifica en 8.5;
  • de lo contrario, si la entidad es un miembro de la variante (9.5), no se realiza ninguna inicialización;
  • de lo contrario, la entidad se inicializa por defecto (8.5).

La Sección 9.4.2 también permite la inicialización en clase de miembros estáticos no const si están marcados con el especificador constexpr .

Entonces, ¿qué pasó con las razones de las restricciones que teníamos en C ++ 03? ¿Simplemente aceptamos las “complicadas reglas del enlazador” o ha cambiado algo más que hace que esto sea más fácil de implementar?

La respuesta corta es que mantuvieron el enlazador sobre el mismo, a expensas de hacer el comstackdor aún más complicado que antes.

Es decir, en lugar de que esto resulte en múltiples definiciones para que el enlazador las resuelva, solo da como resultado una definición, y el comstackdor tiene que ordenarla.

También lleva a reglas un tanto más complejas para que el progtwigdor también se solucione, pero es bastante simple que no sea gran cosa. Las reglas adicionales aparecen cuando tiene dos inicializadores diferentes especificados para un único miembro:

 class X { int a = 1234; public: X() = default; X(int z) : a(z) {} }; 

Ahora, las reglas adicionales en este punto se refieren a qué valor se usa para inicializar a cuando se usa el constructor no predeterminado. La respuesta es bastante simple: si usas un constructor que no especifica ningún otro valor, entonces el 1234 se usaría para inicializar a – pero si usas un constructor que especifica algún otro valor, entonces el 1234 es básicamente ignorado

Por ejemplo:

 #include  class X { int a = 1234; public: X() = default; X(int z) : a(z) {} friend std::ostream &operator<<(std::ostream &os, X const &x) { return os << xa; } }; int main() { X x; X y{5678}; std::cout << x << "\n" << y; return 0; } 

Resultado:

 1234 5678 

Supongo que ese razonamiento podría haber sido escrito antes de que las plantillas estuvieran finalizadas. Después de todo, las “reglas de enlazador complicadas” necesarias para los inicializadores en clase de miembros estáticos ya eran necesarias para C ++ 11 para soportar miembros estáticos de plantillas.

Considerar

 struct A { static int s = ::ComputeSomething(); }; // NOTE: This isn't even allowed, // thanks @Kapil for pointing that out // vs. template  struct B { static int s; } template  int B::s = ::ComputeSomething(); // or template  void Foo() { static int s = ::ComputeSomething(); s++; std::cout << s << "\n"; } 

El problema para el comstackdor es el mismo en los tres casos: ¿en qué unidad de traducción debería emitir la definición de s el código necesario para inicializarlo? La solución simple es emitirlo en todas partes y dejar que el enlazador lo resuelva. Es por eso que los enlazadores ya soportan cosas como __declspec(selectany) . Simplemente no habría sido posible implementar C ++ 03 sin él. Y es por eso que no fue necesario extender el enlazador.

Para decirlo sin rodeos: creo que el razonamiento dado en el viejo estándar es simplemente incorrecto.


ACTUALIZAR

Como señaló Kapil, mi primer ejemplo ni siquiera está permitido en el estándar actual (C ++ 14). Lo dejé de todos modos, porque IMO es el caso más difícil para la implementación (comstackdor, enlazador). Mi punto es: incluso ese caso no es más difícil de lo que ya está permitido, por ejemplo, al usar plantillas.

En teoría So why do these inconvenient restrictions exist?... , So why do these inconvenient restrictions exist?... razón es válida, pero puede evitarse fácilmente y esto es exactamente lo que C ++ 11 hace.

Cuando incluye un archivo, simplemente incluye el archivo y no tiene en cuenta ninguna inicialización. Los miembros se inicializan solo cuando crea una instancia de la clase.

En otras palabras, la inicialización sigue vinculada con el constructor, solo la notación es diferente y es más conveniente. Si no se llama al constructor, los valores no se inicializan.

Si se llama al constructor, los valores se inicializan con una inicialización en clase si está presente o el constructor puede anular eso con la inicialización propia. La ruta de inicialización es esencialmente la misma, es decir, a través del constructor.

Esto es evidente a partir de las propias preguntas frecuentes de Stroustrup sobre C ++ 11.