Błąd podczas używania inicjalizacji w klasie niestatycznego elementu członkowskiego danych i konstruktora klasy zagnieżdżonej

90

Poniższy kod jest dość trywialny i spodziewałem się, że powinien się dobrze skompilować.

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

    B b;

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

Przetestowałem ten kod z g ++ w wersji 4.7.2, 4.8.1, clang ++ 3.2 i 3.3. Oprócz tego, że g ++ 4.7.2 segfaults w tym kodzie ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), inne testowane kompilatory wyświetlają komunikaty o błędach, które niewiele wyjaśniają.

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

Możliwość kompilacji tego kodu jest możliwa i wydaje się, że nie powinno to robić żadnej różnicy. Istnieją dwie możliwości:

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

lub

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

Czy ten kod jest naprawdę nieprawidłowy, czy kompilatory są złe?

etam1024
źródło
3
Mój G ++ 4.7.3 mówi internal compiler error: Segmentation faultdo tego kodu ...
Fred Foo
2
(błąd C2864: „A :: B :: i”: tylko statyczne stałe integralne składowe danych mogą być inicjowane w klasie) tak mówi VC2010. Ten wynik jest zgodny z g ++. Clang też to mówi, chociaż ma to znacznie mniejszy sens. Nie możesz ustawić wartości domyślnej zmiennej w strukturze, robiąc int i = 0to, chyba że tak jest static const int i = 0.
Chris Cooper,
@Borgleader: Swoją drogą, uniknąłbym pokusy traktowania wyrażenia B()jako wywołania funkcji do konstruktora. Ty nigdy bezpośrednio „wezwanie” konstruktor. Pomyśl o tym jako o specjalnej składni, która tworzy tymczasowy B... a konstruktor jest wywoływany jako tylko jedna część tego procesu, głęboko w mechanizmie, który następuje.
Wyścigi lekkości na orbicie,
2
Hmm, dodanie konstruktora do Bwydaje się działać w programie gcc 4.7.
Shafik Yaghmour
7
Co ciekawe, wydaje się, że przeniesienie definicji konstruktora A z A również działa (g ++ 4,7); które gongi z „domyślnym konstruktorem domyślnym nie mogą być używane ... przed końcem definicji klasy”.
moonshadow

Odpowiedzi:

84

Czy ten kod jest naprawdę nieprawidłowy, czy kompilatory są złe?

Cóż, nie. Standard ma wadę - mówi, że zarówno to Ajest uważane za zakończone podczas analizowania inicjatora dla B::i, jak i to B::B()(które używa inicjatora dla B::i) może być użyte w definicji A. To wyraźnie cykliczne. Rozważ to:

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

To ma sprzeczności: B::B()jest niejawnie noexceptIFF A()nie rzucać, a A()nie rzucać IFF B::B()to nie noexcept . Istnieje wiele innych cykli i sprzeczności w tej dziedzinie.

Jest to śledzone przez podstawowe problemy 1360 i 1397 . Zwróćcie w szczególności uwagę na tę uwagę w numerze podstawowym 1397:

Być może najlepszym sposobem rozwiązania tego problemu byłoby uczynienie go źle sformułowanym dla niestatycznego inicjatora elementu członkowskiego danych, aby używał domyślnego konstruktora swojej klasy.

To szczególny przypadek reguły, którą zaimplementowałem w Clang, aby rozwiązać ten problem. Reguła Clanga polega na tym, że domyślny konstruktor domyślny dla klasy nie może być używany przed przeanalizowaniem niestandardowych inicjatorów elementu członkowskiego danych dla tej klasy. Dlatego Clang wydaje tutaj diagnostykę:

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

... ponieważ Clang analizuje domyślne argumenty, zanim Bprzeanalizuje domyślne inicjatory, a ten domyślny argument wymagałby , aby domyślne inicjatory zostały już przeanalizowane (w celu niejawnego zdefiniowania B::B()).

Richarda Smitha
źródło
Dobrze wiedzieć. Jednak komunikat o błędzie nadal wprowadza w błąd, ponieważ konstruktor nie jest w rzeczywistości „używany przez niestatycznego inicjatora elementu członkowskiego danych”.
aschepler
Czy wiesz o tym ze względu na szczególne doświadczenie z tym problemem w przeszłości, czy też po uważnym przeczytaniu normy (i listy usterek)? Ponadto +1.
Cornstalks
+1 za tę szczegółową odpowiedź. Więc jakie byłoby wyjście? Jakiś rodzaj „dwufazowego analizowania klas”, w którym parsowanie elementów składowych klasy zewnętrznej, które są zależne od klas wewnętrznych, jest opóźnione do czasu pełnego uformowania klas wewnętrznych?
TemplateRex,
4
@aschepler Tak, diagnostyka tutaj nie jest zbyt dobra. Złożyłem w tym celu llvm.org/PR16550.
Richard Smith,
@Cornstalks Odkryłem ten problem podczas implementowania inicjatorów dla niestatycznych członków danych w Clang.
Richard Smith,
0

Może to jest problem:

§12.1 5. Domyślny konstruktor, który jest domyślny i nie jest zdefiniowany jako usunięty, jest niejawnie definiowany, gdy jest używany odr (3.2) do tworzenia obiektu typu klasy (1.8) lub gdy jest jawnie domyślny po pierwszej deklaracji

Tak więc domyślny konstruktor jest generowany podczas pierwszego wyszukiwania, ale wyszukiwanie zakończy się niepowodzeniem, ponieważ A nie jest w pełni zdefiniowane, a zatem B wewnątrz A nie zostanie znalezione.

fscan
źródło
Nie jestem pewien co do tego „dlatego”. Oczywiście B bnie stanowi to problemu, a znalezienie jawnych metod / jawnie zadeklarowanego konstruktora w programie Bnie stanowi problemu. Więc byłoby miło zobaczyć jakąś definicję, dlaczego wyszukiwanie powinno przebiegać tutaj inaczej, tak aby „ Bwewnątrz Anie zostało znalezione” tylko w tym jednym przypadku, ale nie w innych, zanim będziemy mogli z tego powodu zadeklarować kod jako nielegalny.
księżycowy cień
Znalazłem słowa w standardzie, że definicja klasy jest uważana za kompletną podczas inicjowania w klasie, w tym w klasach zagnieżdżonych. Nie zawracałem sobie głowy nagrywaniem odniesienia, ponieważ nie wydawało się ono istotne.
Mark B
@moonshadow: instrukcja mówi, że niejawnie domyślne konstruktory są definiowane, gdy używany jest ODR. jawnie jest zdefiniowane po pierwszej deklaracji. A B b nie wywołuje konstruktora, konstruktor A wywołuje konstruktor B
fscan.
Gdyby coś takiego było problemem, kod nadal byłby nieprawidłowy, gdybyś usunął plik =0from i = 0;. Ale bez tego =0kod jest prawidłowy i nie znajdziesz ani jednego kompilatora, który narzekałby na używanie B()w definicji A.
aschepler