Error al usar la inicialización en clase de un miembro de datos no estático y un constructor de clase anidado

90

El siguiente código es bastante trivial y esperaba que se compilara bien.

struct A
{
    struct B
    {
        int i = 0;
    };

    B b;

    A(const B& _b = B())
        : b(_b)
    {}
};

He probado este código con g ++ versión 4.7.2, 4.8.1, clang ++ 3.2 y 3.3. Aparte del hecho de que g ++ 4.7.2 segfaults en este código ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), los otros compiladores probados dan mensajes de error que no explican mucho.

g ++ 4.8.1:

test.cpp: In constructor constexpr A::B::B()’:
test.cpp:3:12: error: constructor required before non-static data member for A::B::i has been parsed
     struct B
            ^
test.cpp: At global scope:
test.cpp:11:23: note: synthesized method constexpr A::B::B()’ first required here 
     A(const B& _b = B())
                       ^

clang ++ 3.2 y 3.3:

test.cpp:11:21: error: defaulted default constructor of 'B' cannot be used by non-static data member initializer which appears before end of class definition
    A(const B& _b = B())
                    ^

Hacer que este código sea compilable es posible y parece que no debería hacer ninguna diferencia. Hay dos opciones:

struct B
{
    int i = 0;
    B(){} // using B()=default; works only for clang++
};

o

struct B
{
    int i;
    B() : i(0) {} // classic c++98 initialization
};

¿Es este código realmente incorrecto o los compiladores están equivocados?

etam1024
fuente
3
Mi G ++ 4.7.3 dice internal compiler error: Segmentation faulta este código ...
Fred Foo
2
(error C2864: 'A :: B :: i': solo los miembros de datos integrales const estáticos pueden inicializarse dentro de una clase) es lo que dice VC2010. Esa salida concuerda con g ++. Clang también lo dice, aunque tiene mucho menos sentido. No puede predeterminar una variable en una estructura haciendo a int i = 0menos que lo sea static const int i = 0.
Chris Cooper
@Borgleader: Por cierto, evitaría la tentación de pensar en la expresión B()como una llamada de función a un constructor. Usted no directamente "llamada" un constructor. Piense en esto como una sintaxis especial que crea un temporal B... y el constructor se invoca como solo una parte de ese proceso, en lo profundo del mecanismo que sigue.
Lightness Races in Orbit
2
Hmm, agregar un constructor Bparece hacer que esto funcione gcc 4.7.
Shafik Yaghmour
7
Curiosamente, mover la definición del constructor de A fuera de A también parece hacer que funcione (g ++ 4.7); que suena con "el constructor predeterminado predeterminado no se puede usar ... antes del final de la definición de clase".
sombra de luna

Respuestas:

84

¿Es este código realmente incorrecto o los compiladores están equivocados?

Bueno, tampoco. El estándar tiene un defecto: dice tanto que Ase considera completo al analizar el inicializador para B::icomo que B::B()(que usa el inicializador para B::i) se puede usar dentro de la definición de A. Eso es claramente cíclico. Considera esto:

struct A {
  struct B {
    int i = (A(), 0);
  };
  A() noexcept(!noexcept(B()));
};

Esto tiene una contradicción: B::B()es implícitamente noexceptiff A()no lanza, y A()no lanza iff no loB::B() es . Hay varios otros ciclos y contradicciones en esta área. noexcept

Esto se rastrea mediante los problemas centrales 1360 y 1397 . Tenga en cuenta en particular esta nota en el número principal 1397:

Quizás la mejor manera de abordar esto sería hacer que un inicializador de miembros de datos no estáticos no esté bien formado para usar un constructor predeterminado de su clase.

Ese es un caso especial de la regla que implementé en Clang para resolver este problema. La regla de Clang es que un constructor predeterminado predeterminado para una clase no se puede usar antes de que se analicen los inicializadores de miembros de datos no estáticos para esa clase. Por lo tanto, Clang emite un diagnóstico aquí:

    A(const B& _b = B())
                    ^

... porque Clang analiza los argumentos predeterminados antes de analizar los inicializadores predeterminados, y este argumento predeterminado requeriría que Blos inicializadores predeterminados ya se hayan analizado (para poder definirlos implícitamente B::B()).

Richard Smith
fuente
Bueno saber. Pero el mensaje de error sigue siendo engañoso, ya que el constructor no es "utilizado por el inicializador de miembros de datos no estáticos".
aschepler
¿Sabía esto debido a una experiencia pasada en particular con este problema, o simplemente por leer cuidadosamente el estándar (y la lista de defectos)? Además, +1.
Cornstalks
+1 para esta respuesta detallada. Entonces, ¿cuál sería la salida? ¿Algún tipo de "análisis de clase de 2 fases", donde el análisis de los miembros de la clase externa que dependen de las clases internas se retrasa hasta que las clases internas se hayan formado por completo?
TemplateRex
4
@aschepler Sí, el diagnóstico aquí no es muy bueno. He presentado llvm.org/PR16550 para eso.
Richard Smith
@Cornstalks Descubrí este problema al implementar inicializadores para miembros de datos no estáticos en Clang.
Richard Smith
0

Tal vez éste es el problema:

§12.1 5. Un constructor predeterminado que está predeterminado y no definido como eliminado se define implícitamente cuando se usa odr (3.2) para crear un objeto de su tipo de clase (1.8) o cuando está explícitamente predeterminado después de su primera declaración.

Entonces, el constructor predeterminado se genera cuando se busca por primera vez, pero la búsqueda fallará porque A no está completamente definido y, por lo tanto, B dentro de A no se encontrará.

fscan
fuente
No estoy seguro de eso "por lo tanto". Claramente B bno es un problema, y ​​encontrar métodos explícitos / un constructor declarado explícitamente Bno es un problema. Por lo que sería bueno ver una definición de por qué la búsqueda debe proceder de manera diferente aquí, así que " Ben el interior Ano se encuentra" en tan sólo este único caso, pero no los otros, antes de que podamos declarar el código ilegal por esta razón.
sombra de luna
Encontré palabras en el estándar de que la definición de clase se considera completa durante la inicialización de la clase, incluso dentro de las clases anidadas. No me molesté en registrar la referencia porque no parecía relevante.
Mark B
@moonshadow: la declaración dice que los constructores implícitamente predeterminados se definen cuando se usa odr-. explícitamente se define después de la primera declaración. Y B b no llama a un constructor, el constructor de A llama al constructor de B
fscan
Si algo como esto fuera el problema, el código aún no sería válido si elimina el =0archivo i = 0;. Pero sin eso =0, el código es válido y no encontrará un solo compilador que se queje del uso B()dentro de la definición de A.
aschepler