Erreur lors de l'utilisation de l'initialisation en classe du membre de données non statique et du constructeur de classe imbriquée

90

Le code suivant est assez trivial et je m'attendais à ce qu'il se compile correctement.

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

    B b;

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

J'ai testé ce code avec g ++ version 4.7.2, 4.8.1, clang ++ 3.2 et 3.3. Hormis le fait que g ++ 4.7.2 segfaut sur ce code ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), les autres compilateurs testés donnent des messages d'erreur qui n'expliquent pas grand chose.

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 et 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())
                    ^

Rendre ce code compilable est possible et ne devrait faire aucune différence. Il existe deux options:

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

ou

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

Ce code est-il vraiment incorrect ou les compilateurs se trompent-ils?

etam1024
la source
3
Mon G ++ 4.7.3 dit internal compiler error: Segmentation faultà ce code ...
Fred Foo
2
(erreur C2864: 'A :: B :: i': seuls les membres de données intégrales const statiques peuvent être initialisés dans une classe) est ce que VC2010 dit. Cette sortie est en accord avec g ++. Clang le dit aussi, bien que cela ait beaucoup moins de sens. Vous ne pouvez pas par défaut une variable dans une structure en faisant à int i = 0moins que ce ne soit le cas static const int i = 0.
Chris Cooper
@Borgleader: BTW, j'éviterais la tentation de considérer l'expression B()comme un appel de fonction à un constructeur. Vous n'appelez jamais directement un constructeur. Considérez cela comme une syntaxe spéciale qui crée un temporaire B... et le constructeur est invoqué comme une partie de ce processus, au plus profond du mécanisme qui suit.
Courses de légèreté en orbite le
2
Hmm, l'ajout d'un constructeur Bsemble faire fonctionner cela gcc 4.7.
Shafik Yaghmour
7
Fait intéressant, déplacer la définition du constructeur de A hors de A semble également le faire fonctionner (g ++ 4.7); qui carillon avec "le constructeur par défaut par défaut ne peut pas être utilisé ... avant la fin de la définition de classe".
moonshadow le

Réponses:

84

Ce code est-il vraiment incorrect ou les compilateurs se trompent-ils?

Eh bien non plus. Le standard a un défaut - il dit que les deux Asont considérés comme terminés lors de l'analyse de l'initialiseur pour B::i, et que B::B()(qui utilise l'initialiseur pour B::i) peuvent être utilisés dans la définition de A. C'est clairement cyclique. Considère ceci:

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

Cela a une contradiction: B::B()est implicitement noexceptssi A()ne lance pas et A()ne lance pas ssi B::B()n'est pas noexcept . Il existe un certain nombre d'autres cycles et contradictions dans ce domaine.

Ceci est suivi par les problèmes principaux 1360 et 1397 . Notez en particulier cette note dans le numéro central 1397:

La meilleure façon de résoudre ce problème serait peut-être de le rendre mal formé pour qu'un initialiseur de membre de données non statique utilise un constructeur par défaut de sa classe.

C'est un cas particulier de la règle que j'ai implémentée dans Clang pour résoudre ce problème. La règle de Clang est qu'un constructeur par défaut par défaut pour une classe ne peut pas être utilisé avant que les initialiseurs de membres de données non statiques pour cette classe soient analysés. Par conséquent, Clang émet un diagnostic ici:

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

... parce que Clang analyse les arguments par défaut avant d'analyser les initialiseurs par défaut, et que cet argument par défaut exigerait que Bles initialiseurs par défaut aient déjà été analysés (afin de les définir implicitement B::B()).

Richard Smith
la source
Bon à savoir. Mais le message d'erreur est toujours trompeur, puisque le constructeur n'est en fait pas "utilisé par l'initialiseur de membre de données non statique".
aschepler
Le saviez-vous en raison d'une expérience antérieure particulière avec ce problème, ou simplement en lisant attentivement la norme (et la liste des défauts)? Aussi, +1.
Cornstalks
+1 pour cette réponse détaillée. Alors, quelle serait la solution? Une sorte "d'analyse de classe en 2 phases", où l'analyse des membres de classe externes qui dépendent des classes internes est retardée jusqu'à ce que les classes internes aient été complètement formées?
TemplateRex
4
@aschepler Oui, le diagnostic ici n'est pas très bon. J'ai déposé llvm.org/PR16550 pour cela.
Richard Smith
@Cornstalks J'ai découvert ce problème lors de l'implémentation d'initialiseurs pour les membres de données non statiques dans Clang.
Richard Smith
0

C'est peut-être le problème:

§12.1 5. Un constructeur par défaut qui est par défaut et non défini comme supprimé est implicitement défini quand il est utilisé odr (3.2) pour créer un objet de son type de classe (1.8) ou quand il est explicitement par défaut après sa première déclaration

Ainsi, le constructeur par défaut est généré lors de la première recherche, mais la recherche échouera car A n'est pas complètement défini et B à l'intérieur de A ne sera donc pas trouvé.

fscan
la source
Je ne suis pas sûr de cela "donc". Ce B bn'est clairement pas un problème, et trouver des méthodes explicites / un constructeur explicitement déclaré dans Bn'est pas un problème. Il serait donc bien de voir une définition de la raison pour laquelle la recherche devrait se dérouler différemment ici afin que "l' Bintérieur Ane soit pas trouvé" dans un seul cas mais pas dans les autres, avant de pouvoir déclarer le code illégal pour cette raison.
moonshadow
J'ai trouvé des mots dans la norme indiquant que la définition de classe est considérée comme complète lors de l'initialisation en classe, y compris dans les classes imbriquées. Je n'ai pas pris la peine d'enregistrer la référence car elle ne semblait pas pertinente.
Mark B
@moonshadow: l'instruction indique que les constructeurs implicitement par défaut sont définis lors de l'utilisation d'odr. explicitement est défini après la première déclaration. Et B b n'appelle pas de constructeur, le constructeur de A appelle le constructeur de B
fscan
Si quelque chose de ce genre était le problème, le code serait toujours invalide si vous supprimez le =0de i = 0;. Mais sans cela =0, le code est valide et vous ne trouverez pas un seul compilateur qui se plaint de l'utilisation B()dans la définition de A.
aschepler