¿Por qué mi clase no es construible por defecto?

28

Tengo esas clases:

#include <type_traits>

template <typename T>
class A {
public:
    static_assert(std::is_default_constructible_v<T>);

};

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

    A<C> a_m;
};

int main() {
    A<B::C> a;
}

Al compilar, a_mno se puede construir por defecto, pero lo aes.

Al cambiar Ca:

struct C {
      int i;
   };

todo esta bien.

Probado con Clang 9.0.0.

Nicolas
fuente
3
GCC 8.3 - OK, GCC 9.1 / 9.2 - Fail.
Evg
2
Con C() {}esto también funciona.
Evg
3
Esto me huele a buggy. No hay partido inmediatamente obvio en Bugzilla.
Carreras de ligereza en órbita el
2
Interesante: el static_assertin Afalla, pero si por defecto construyes un Tinterior de A(por ejemplo, pones un miembro T t;allí), todo funciona bien. Una inconsistencia entre lo que es realmente posible el tipo de rasgo que está diciendo y lo que ...
sebrockm
2
@Nicolas True, pero eso se debe a algunos casos extremos, ninguno de los cuales se aplica aquí (en particular, como dice la misma oración en cppreference, const int x;no es válido sin un inicializador, simplemente debido al constcomportamiento de inicialización de los tipos incorporados y algunos historia)
Carreras de ligereza en órbita el

Respuestas:

9

Esto no está permitido tanto por el texto del estándar como por varias implementaciones importantes como se señala en los comentarios, pero por razones completamente ajenas.

Primero, la razón "por el libro": el punto de instanciación de A<C>es, según el estándar, inmediatamente antes de la definición deB , y el punto de instanciación de std::is_default_constructible<C>es inmediatamente anterior a eso:

Para una especialización de plantilla de clase, [...] si la especialización se instancia implícitamente porque se hace referencia desde otra especialización de plantilla, si el contexto desde el que se hace referencia a la especialización depende de un parámetro de plantilla, y si la especialización no se instancia previamente para la instanciación de la plantilla adjunta, el punto de instanciación es inmediatamente anterior al punto de instanciación de la plantilla adjunta. De lo contrario, el punto de instanciación para tal especialización precede inmediatamente a la declaración o definición del alcance del espacio de nombres que se refiere a la especialización.

Como Cestá claramente incompleto en ese punto, el comportamiento de la creación de instancias std::is_default_constructible<C>no está definido. Sin embargo, vea el problema central 287 , que cambiaría esta regla.


En realidad, esto tiene que ver con el NSDMI.

  • Los NSDMI son extraños porque se retrasan el análisis, o en el lenguaje estándar son un "contexto de clase completa".
  • Por lo tanto, eso = 0podría referirse en principio a cosas Baún no declaradas, por lo que la implementación realmente no puede tratar de analizarlo hasta que haya terminado B.
  • Completar una clase requiere la declaración implícita de funciones miembro especiales, en particular el constructor predeterminado, ya Cque no tiene un constructor declarado.
  • Partes de esa declaración (constexpr-ness, noexcept-ness) dependen de las propiedades del NSDMI.
  • Por lo tanto, si el compilador no puede analizar el NSDMI, no puede completar la clase.
  • Como resultado, en el momento en que A<C>se crea una instancia , piensa que Cestá incompleto.

Toda esta área que se ocupa de las regiones analizadas de forma diferida es lamentablemente poco especificada, con la divergencia de implementación que la acompaña. Puede pasar un tiempo antes de que se limpie.

TC
fuente
0

Comportamiento indefinido es:

Si una instanciación de una plantilla anterior depende, directa o indirectamente, de un tipo incompleto, y esa instanciación podría producir un resultado diferente si ese tipo se completara hipotéticamente, el comportamiento es indefinido.

Olvido
fuente
77
¿Por qué está C incompleta?
Interjay
1
@interjay, Cestá completo, pero Bno lo está. Y B::Cdepende indirectamente de B.
Evg
1
@Evg El texto "Depende directa o indirectamente" solo aparece en cppreference.com. El estándar solo dice que el tipo T debe estar completo.
Interjay
2
@interjay Escribí la mayor parte de esta redacción. Lo que estamos tratando de decir es que 1) si crea una instancia de un rasgo de una manera que podría provocar una violación de ODR más adelante cuando se complete algún tipo incompleto, eso no está definido; y 2) no está definido, incluso si en realidad no causa una violación de ODR en su programa, de modo que las implementaciones estándar de la biblioteca pueden elegir diagnosticar que en el momento en que se utiliza el rasgo si así lo desean. Si Ctiene una plantilla de constructor predeterminada con algunos SFINAE extraños que pueden cambiar las respuestas si Bse completa de manera diferente, entonces seguro, el rasgo depende de ello.
TC