Pourquoi ma classe n'est-elle pas constructible par défaut?

28

J'ai ces cours:

#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;
}

Lors de la compilation, a_mn'est pas constructible par défaut mais l' aest.

Lors du passage Cà:

struct C {
      int i;
   };

tout va bien.

Testé avec Clang 9.0.0.

Nicolas
la source
3
GCC 8.3 - OK, GCC 9.1 / 9.2 - Échec.
Evg
2
Avec C() {}ça marche aussi.
Evg
3
Ça sent le buggy pour moi. Aucun match immédiatement évident sur Bugzilla.
Courses de légèreté en orbite
2
Intéressant: static_assertin Aéchoue, mais si vous construisez par défaut un Tintérieur de A(par exemple, mettez un membre T t;là-bas), cela fonctionne très bien. Une incohérence entre ce que le trait de type vous dit et ce qui est réellement possible ...
sebrockm
2
@Nicolas True, mais c'est à cause de certains cas marginaux, dont aucun ne s'applique ici (en particulier, comme le dit la même phrase sur cppreference, const int x;n'est pas valide sans initialiseur, uniquement en raison constdu comportement d'initialisation des types intégrés et de certains historique)
Courses de légèreté en orbite

Réponses:

9

Ceci est interdit à la fois par le texte de la norme et par plusieurs mises en œuvre majeures comme indiqué dans les commentaires, mais pour des raisons totalement indépendantes.

Premièrement, la raison "par le livre": le point d'instanciation de A<C>est, selon la norme, immédiatement avant la définition deB , et le point d'instanciation de std::is_default_constructible<C>est immédiatement avant cela:

Pour une spécialisation de modèle de classe, [...] si la spécialisation est implicitement instanciée parce qu'elle est référencée à partir d'une autre spécialisation de modèle, si le contexte à partir duquel la spécialisation est référencée dépend d'un paramètre de modèle, et si la spécialisation n'est pas instanciée précédente à l'instanciation du modèle englobant, le point d'instanciation est immédiatement avant le point d'instanciation du modèle englobant. Sinon, le point d'instanciation d'une telle spécialisation précède immédiatement la déclaration ou la définition de la portée de l'espace de noms qui fait référence à la spécialisation.

Puisqu'il Cest clairement incomplet à ce stade, le comportement d'instanciation std::is_default_constructible<C>n'est pas défini. Voir toutefois le problème central 287 , qui modifierait cette règle.


En réalité, cela a à voir avec le NSDMI.

  • Les NSDMI sont bizarres car ils sont analysés différés - ou dans le langage standard, ils sont un "contexte de classe complète".
  • Ainsi, cela = 0pourrait en principe se référer à des choses qui Bne sont pas encore déclarées, donc l'implémentation ne peut pas vraiment essayer de l'analyser jusqu'à ce qu'elle soit terminée B.
  • Compléter une classe nécessite la déclaration implicite de fonctions membres spéciales, en particulier le constructeur par défaut, car Caucun constructeur n'a été déclaré.
  • Certaines parties de cette déclaration (constexpr-ness, noexcept-ness) dépendent des propriétés du NSDMI.
  • Ainsi, si le compilateur ne peut pas analyser le NSDMI, il ne peut pas terminer la classe.
  • Du coup, au moment où elle s'instancie A<C>, elle pense que Cc'est incomplet.

L'ensemble de ce domaine traitant des régions analysées avec retard est malheureusement sous-spécifié, avec des divergences de mise en œuvre. Cela peut prendre un certain temps avant d'être nettoyé.

TC
la source
0

Comportement indéfini c'est:

Si une instanciation d'un modèle ci-dessus dépend, directement ou indirectement, d'un type incomplet, et que cette instanciation pourrait donner un résultat différent si ce type était hypothétiquement terminé, le comportement n'est pas défini.

Oubli
la source
7
Pourquoi C est-il incomplet?
interjay
1
@interjay, Cest terminé, mais Bne l'est pas. Et B::Cdépend indirectement de B.
Evg
1
@Evg Le texte "Dépend directement ou indirectement" n'apparaît que sur cppreference.com. La norme dit simplement que le type T doit être complet.
interjay
2
@interjay J'ai écrit la plupart de ces mots. Ce que nous essayons de dire, c'est que 1) si vous instanciez un trait d'une manière qui pourrait provoquer une violation ODR plus tard quand un type incomplet est terminé, ce n'est pas défini; et 2) il n'est pas défini même si vous ne provoquez pas réellement une violation ODR dans votre programme, de sorte que les implémentations de bibliothèque standard puissent choisir de diagnostiquer qu'au moment où le trait est utilisé s'ils le souhaitent. Si Ca un modèle de constructeur par défaut avec un SFINAE étrange qui peut changer les réponses s'il Best complété différemment, alors bien sûr, le trait en dépend.
TC