Fehler bei Verwendung der In-Class-Initialisierung des nicht statischen Datenelements und des verschachtelten Klassenkonstruktors


89

Der folgende Code ist ziemlich trivial und ich habe erwartet, dass er gut kompiliert werden kann.

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

    B b;

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

Ich habe diesen Code mit g ++ Version 4.7.2, 4.8.1, clang ++ 3.2 und 3.3 getestet. Abgesehen von der Tatsache, dass g ++ 4.7.2 Fehler in diesem Code aufweist ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), geben die anderen getesteten Compiler Fehlermeldungen aus, die nicht viel erklären.

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

Es ist möglich, diesen Code kompilierbar zu machen, und es scheint, dass dies keinen Unterschied machen sollte. Es gibt zwei Möglichkeiten:

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

oder

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

Ist dieser Code wirklich falsch oder sind die Compiler falsch?


3
Mein G ++ 4.7.3 sagt internal compiler error: Segmentation faultzu diesem Code ...
Fred Foo

2
(Fehler C2864: 'A :: B :: i': Innerhalb einer Klasse können nur statische konstante Integraldatenelemente initialisiert werden), sagt VC2010. Diese Ausgabe stimmt mit g ++ überein. Clang sagt es auch, obwohl es viel weniger Sinn macht. Sie können eine Variable in einer Struktur nur dann als Standard festlegen, int i = 0wenn dies der Fall ist static const int i = 0.
Chris Cooper

@ Borgleader: Übrigens würde ich die Versuchung vermeiden, den Ausdruck B()als Funktionsaufruf für einen Konstruktor zu betrachten. Sie "rufen" niemals direkt einen Konstruktor auf. Stellen Sie sich dies als eine spezielle Syntax vor, die ein temporäres Element erstellt B... und der Konstruktor wird nur als ein Teil dieses Prozesses aufgerufen, tief innerhalb des folgenden Mechanismus.
Leichtigkeitsrennen im Orbit

2
Hmm, das Hinzufügen eines Konstruktors zu Bscheint diese Arbeit in zu machen gcc 4.7.
Shafik Yaghmour

7
Interessanterweise scheint es auch zu funktionieren, wenn die Definition des Konstruktors von A aus A verschoben wird (g ++ 4.7). Welches Glockenspiel mit "Standard-Standardkonstruktor kann nicht verwendet werden ... vor dem Ende der Klassendefinition".
Mondschatten

Antworten:


84

Ist dieser Code wirklich falsch oder sind die Compiler falsch?

Nun, auch nicht. Der Standard weist einen Fehler auf - er besagt, dass sowohl das, Awas beim Parsen des Initialisierers als vollständig betrachtet wird, als auch das, für B::idas B::B()(der den Initialisierer verwendet B::i) verwendet werden können, innerhalb der Definition von verwendet werden können A. Das ist eindeutig zyklisch. Bedenken Sie:

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

Dies hat einen Widerspruch: B::B()implizit noexceptiff A()nicht werfen, und A()wirft nicht iff B::B()ist nicht noexcept . Es gibt eine Reihe anderer Zyklen und Widersprüche in diesem Bereich.

Dies wird durch die Kernprobleme 1360 und 1397 verfolgt . Beachten Sie insbesondere diesen Hinweis in der Kernausgabe 1397:

Möglicherweise besteht die beste Möglichkeit, dies zu beheben, darin, dass ein nicht statischer Datenelementinitialisierer einen standardmäßigen Konstruktor seiner Klasse verwendet.

Dies ist ein Sonderfall der Regel, die ich in Clang implementiert habe, um dieses Problem zu beheben. Clangs Regel lautet, dass ein standardmäßiger Standardkonstruktor für eine Klasse nicht verwendet werden kann, bevor die nicht statischen Datenelementinitialisierer für diese Klasse analysiert wurden. Daher stellt Clang hier eine Diagnose:

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

... weil Clang Standardargumente analysiert, bevor es Standardinitialisierer analysiert, und dieses Standardargument erfordert B, dass die Standardinitialisierer bereits analysiert wurden (um implizit zu definieren B::B()).


Gut zu wissen. Die Fehlermeldung ist jedoch immer noch irreführend, da der Konstruktor tatsächlich nicht "vom nicht statischen Datenelementinitialisierer verwendet" wird.
Aschepler

Wussten Sie dies aufgrund einer bestimmten Erfahrung in der Vergangenheit mit diesem Problem oder einfach durch sorgfältiges Lesen des Standards (und der Liste der Mängel)? Auch +1.
Cornstalks

+1 für diese detaillierte Antwort. Also, was wäre der Ausweg? Eine Art "2-Phasen-Klassenanalyse", bei der die Analyse von Mitgliedern der äußeren Klasse, die von inneren Klassen abhängen, verzögert wird, bis die inneren Klassen vollständig gebildet sind?
TemplateRex

4
@aschepler Ja, die Diagnose hier ist nicht sehr gut. Ich habe llvm.org/PR16550 dafür eingereicht.
Richard Smith

@Cornstalks Ich habe dieses Problem beim Implementieren von Initialisierern für nicht statische Datenelemente in Clang entdeckt.
Richard Smith

0

Vielleicht ist das das Problem:

§12.1 5. Ein Standardkonstruktor, der voreingestellt und nicht als gelöscht definiert ist, wird implizit definiert, wenn er zum Erstellen eines Objekts seines Klassentyps (1.8) verwendet wird (3.2) oder wenn er nach seiner ersten Deklaration explizit voreingestellt wird

Der Standardkonstruktor wird also beim ersten Nachschlagen generiert, aber das Nachschlagen schlägt fehl, da A nicht vollständig definiert ist und B in A daher nicht gefunden wird.


Da bin ich mir "deshalb" nicht sicher. Dies B bist eindeutig kein Problem, und das Auffinden expliziter Methoden / eines explizit deklarierten Konstruktors in Bist kein Problem. Es wäre also schön zu sehen, warum die Suche hier anders ablaufen sollte, damit " Binnen Anicht gefunden wird", nur in diesem einen Fall, aber nicht in den anderen, bevor wir den Code aus diesem Grund für illegal erklären können.
Mondschatten

Ich habe im Standard Wörter gefunden, die besagen, dass die Klassendefinition während der Initialisierung in der Klasse als vollständig angesehen wird, auch innerhalb verschachtelter Klassen. Ich habe mir nicht die Mühe gemacht, die Referenz aufzuzeichnen, da sie nicht relevant schien.
Mark B

@moonshadow: Die Anweisung besagt, dass implizit voreingestellte Konstruktoren definiert werden, wenn odr verwendet wird. wird explizit nach der ersten Deklaration definiert. Und B b ruft keinen Konstruktor auf, der Konstruktor von A ruft den Konstruktor von B
fscan

Wenn so etwas das Problem wäre, wäre der Code immer noch ungültig, wenn Sie den =0von entfernen i = 0;. Aber ohne das =0ist der Code gültig und Sie werden keinen einzigen Compiler finden, der sich über die Verwendung B()innerhalb der Definition von beschwert A.
Aschepler
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.