Помилка під час використання в класі ініціалізації нестатичного члена даних та конструктора вкладеного класу


90

Наступний код досить тривіальний, і я очікував, що він повинен скомпілюватись добре.

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

    B b;

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

Я протестував цей код за допомогою g ++ версії 4.7.2, 4.8.1, clang ++ 3.2 та 3.3. Окрім того, що g ++ 4.7.2 здійснює сегментацію цього коду ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), інші перевірені компілятори видають повідомлення про помилки, які мало що пояснюють.

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

Зробити цей код компілювальним можливо, і, здається, він не повинен мати ніякої різниці. Є два варіанти:

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

або

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

Цей код насправді неправильний чи компілятори помиляються?


3
Мій G ++ 4.7.3 говорить internal compiler error: Segmentation faultдо цього коду ...
Fred Foo 02

2
(помилка C2864: 'A :: B :: i': у класі можуть бути ініціалізовані лише статичні інтегральні елементи даних const) - це те, що говорить VC2010. Цей результат відповідає g ++. Клан це теж говорить, хоча це має набагато менший сенс. Ви не можете встановити змінну в структурі за замовчуванням, int i = 0якщо це не так static const int i = 0.
Кріс Купер

@Borgleader: До речі, я б уникнув спокуси думати про вираз B()як про виклик функції до конструктора. Ви ніколи безпосередньо не "називаєте" конструктор. Подумайте про це як про спеціальний синтаксис, який створює тимчасовий B... і конструктор викликається як лише одна частина цього процесу, глибоко всередині механізму, який слід.
Гонки легкості на орбіті

2
Хм, додавання конструктора до, Bздається, робить цю роботу в gcc 4.7.
Шафік Ягмор

7
Цікаво, що переміщення визначення конструктора A з A також, здається, змушує його працювати (g ++ 4.7); які куранти з "конструктором за замовчуванням за замовчуванням не можна використовувати ... до кінця визначення класу".
moonshadow

Відповіді:


84

Цей код насправді неправильний чи компілятори помиляються?

Ну, ні. Стандарт має дефект - він говорить і про те, що Aвважається завершеним під час розбору ініціалізатора для B::i, і про те, B::B()(що використовує ініціалізатор для B::i), можна використовувати у визначенні A. Це явно циклічно. Розглянемо це:

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

Це протиріччя: B::B()неявно noexceptтоді і тільки тоді A()не залишити, і A()не кидайте тоді і тільки тоді B::B()це НЕ noexcept . У цій галузі існує ряд інших циклів та суперечностей.

Це відстежується за основними проблемами 1360 та 1397 . Зауважте, зокрема, цю примітку в основному випуску 1397:

Можливо, найкращим способом вирішення цього було б зробити неправильно сформовану для нестатичного ініціалізатора даних даних використання конструктора за замовчуванням для свого класу.

Це особливий випадок правила, яке я застосував у Clang для вирішення цієї проблеми. Правило Кланга полягає в тому, що конструктор за замовчуванням за замовчуванням для класу не може використовуватися до синтаксичного аналізу нестатичних ініціалізаторів даних для цього класу. Отже, Клан видає тут діагностику:

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

... тому що Clang аналізує аргументи за замовчуванням перед тим, як він аналізує ініціалізатори за замовчуванням, і для цього аргументу за Bзамовчуванням потрібно буде проаналізувати ініціатори за замовчуванням (для неявного визначення B::B()).


Добре знати. Але повідомлення про помилку все ще вводить в оману, оскільки конструктор насправді не "використовується нестатичним ініціалізатором даних".
aschepler 02

Чи знали ви це через певний минулий досвід роботи з цією проблемою або просто уважно прочитавши стандарт (і перелік дефектів)? Також +1.
Cornstalks

+1 за цю детальну відповідь. То який би був вихід? Якийсь "2-фазний синтаксичний розбір класу", де синтаксичний розбір зовнішніх членів класу, які залежать від внутрішніх класів, затримується, поки внутрішні класи не сформуються повністю?
TemplateRex

4
@aschepler Так, діагностика тут не дуже хороша. Для цього я подав llvm.org/PR16550.
Richard Smith

@Cornstalks Я виявив цю проблему під час реалізації ініціалізаторів для нестатичних членів даних у Clang.
Richard Smith

0

Можливо, це проблема:

§12.1 5. Конструктор за замовчуванням, який за замовчуванням не визначений як видалений, неявно визначається, коли його підтримують (3.2) для створення об’єкта типу свого класу (1.8) або коли він явно за замовчуванням після першого оголошення

Отже, конструктор за замовчуванням генерується при першому пошуку, але пошук не вдасться, оскільки A не повністю визначений, і B всередині A не буде знайдено.


Я не впевнений у цьому "тому". Очевидно, що B bце не проблема, і пошук явних методів / явно оголошеного конструктора в Bне є проблемою. Тож було б непогано побачити деяке визначення того, чому пошук повинен виконуватися тут інакше, щоб « Bвсередині Aне знайдено» лише в цьому випадку, але не в інших, перш ніж ми можемо оголосити код незаконним з цієї причини.
Moonshadow

Я знайшов у стандарті слова, що визначення класу вважається завершеним під час ініціалізації в класі, включаючи вкладені класи. Я не потрудився записати посилання, оскільки воно не здавалося актуальним.
Mark B

@moonshadow: у заяві сказано, що неявно дефолтні конструктори визначаються при використанні odr-. явно визначається після першої декларації. І B b не викликає конструктор, конструктор A викликає конструктор B
fscan

Якби щось подібне було проблемою, код все одно був би недійсним, якщо ви видалите =0з i = 0;. Але без цього =0код дійсний, і ви не знайдете жодного компілятора, який би скаржився на використання B()в межах визначення A.
aschepler
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.