Errore durante l'utilizzo dell'inizializzazione in classe del membro dati non statico e del costruttore di classi nidificato


90

Il codice seguente è abbastanza banale e mi aspettavo che si compilasse correttamente.

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

    B b;

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

Ho testato questo codice con g ++ versione 4.7.2, 4.8.1, clang ++ 3.2 e 3.3. Oltre al fatto che g ++ 4.7.2 segfault su questo codice ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), gli altri compilatori testati danno messaggi di errore che non spiegano molto.

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

Rendere questo codice compilabile è possibile e sembra che non dovrebbe fare differenza. Ci sono due opzioni:

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

Questo codice è davvero errato o i compilatori sono sbagliati?


3
Il mio G ++ 4.7.3 dice internal compiler error: Segmentation faulta questo codice ...
Fred Foo

2
(errore C2864: 'A :: B :: i': solo i membri di dati integrali const statici possono essere inizializzati all'interno di una classe) è ciò che dice VC2010. Quell'output concorda con g ++. Lo dice anche Clang, anche se ha molto meno senso. Non puoi impostare come predefinito una variabile in una struttura a int i = 0meno che non lo sia static const int i = 0.
Chris Cooper

@ Borgleader: BTW eviterei la tentazione di pensare all'espressione B()come una chiamata di funzione a un costruttore. Non si "chiama mai " direttamente un costruttore. Pensa a questa come una sintassi speciale che crea un temporaneo B... e il costruttore viene invocato solo come una parte di quel processo, nel profondo del meccanismo che segue.
Gare di leggerezza in orbita il

2
Hmm, l'aggiunta di un costruttore a Bsembra far funzionare tutto questo gcc 4.7.
Shafik Yaghmour

7
È interessante notare che anche spostare la definizione del costruttore di A da A sembra farlo funzionare (g ++ 4.7); che suona con "il costruttore predefinito predefinito non può essere utilizzato ... prima della fine della definizione della classe".
Moonshadow

Risposte:


84

Questo codice è davvero errato o i compilatori sono sbagliati?

Ebbene, nessuno dei due. Lo standard ha un difetto: dice sia che Aè considerato completo durante l'analisi dell'inizializzatore per B::i, sia che quello B::B()(che usa l'inizializzatore per B::i) può essere usato all'interno della definizione di A. È chiaramente ciclico. Considera questo:

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

Ciò ha una contraddizione: B::B()è implicitamente noexceptse A()e solo se non lancia e A()non lancia se B::B()e solo se non lo è noexcept. Ci sono una serie di altri cicli e contraddizioni in quest'area.

Questo è tracciato dai problemi principali 1360 e 1397 . Nota in particolare questa nota nel numero principale 1397:

Forse il modo migliore per risolvere questo problema sarebbe rendere mal formato per un inizializzatore di membri dati non statici l'utilizzo di un costruttore predefinito della sua classe.

Questo è un caso speciale della regola che ho implementato in Clang per risolvere questo problema. La regola di Clang è che un costruttore predefinito predefinito per una classe non può essere utilizzato prima che gli inizializzatori dei membri dati non statici per quella classe vengano analizzati. Quindi Clang emette una diagnostica qui:

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

... perché Clang analizza gli argomenti predefiniti prima di analizzare gli inizializzatori predefiniti, e questo argomento predefinito richiederebbe che Bgli inizializzatori predefiniti di s siano già stati analizzati (per definirli implicitamente B::B()).


Buono a sapersi. Ma il messaggio di errore è ancora fuorviante, poiché il costruttore non è infatti "utilizzato dall'inizializzatore di membri dati non statici".
aschepler

Lo sapevi a causa di una particolare esperienza passata con questo problema, o semplicemente leggendo attentamente lo standard (e l'elenco dei difetti)? Inoltre, +1.
Steli di mais

+1 per questa risposta dettagliata. Allora quale sarebbe la via d'uscita? Una sorta di "analisi delle classi a 2 fasi", in cui l'analisi dei membri della classe esterna che dipendono dalle classi interne viene ritardata fino a quando le classi interne non sono state completamente formate?
TemplateRex

4
@aschepler Sì, la diagnostica qui non è molto buona. Ho archiviato llvm.org/PR16550 per questo.
Richard Smith

@ Cornstalks Ho scoperto questo problema durante l'implementazione degli inizializzatori per i membri di dati non statici in Clang.
Richard Smith

0

Forse questo è il problema:

§12.1 5. Un costruttore predefinito che è predefinito e non definito come cancellato è implicitamente definito quando è usato odr (3.2) per creare un oggetto del suo tipo di classe (1.8) o quando è esplicitamente predefinito dopo la sua prima dichiarazione

Quindi, il costruttore predefinito viene generato alla prima ricerca, ma la ricerca fallirà perché A non è completamente definito e B all'interno di A non verrà quindi trovato.


Non sono sicuro di questo "quindi". Chiaramente B bnon è un problema, e trovare metodi espliciti / un costruttore dichiarato esplicitamente in Bnon è un problema. Quindi sarebbe bello vedere alcune definizioni del motivo per cui la ricerca dovrebbe procedere in modo diverso qui in modo che " Bdentro Anon si trovi" solo in questo caso ma non negli altri, prima di poter dichiarare il codice illegale per questo motivo.
Moonshadow

Ho trovato parole nello standard che la definizione di classe è considerata completa durante l'inizializzazione in classe, anche all'interno di classi nidificate. Non mi sono preoccupato di registrare il riferimento perché non sembrava rilevante.
Mark B

@moonshadow: la dichiarazione dice che i costruttori implicitamente predefiniti sono definiti quando odr- usato. esplicitamente è definito dopo la prima dichiarazione. E B b non chiama un costruttore, il costruttore di A chiama il costruttore di B
fscan

Se qualcosa del genere fosse il problema, il codice sarebbe comunque non valido se rimuovi il file =0da i = 0;. Ma senza questo =0, il codice è valido e non troverai un singolo compilatore che si lamenta di usare B()all'interno della definizione di A.
aschepler
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.