Ошибка при использовании внутриклассовой инициализации нестатического члена данных и конструктора вложенного класса

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

Это действительно неправильный код или компиляторы ошибаются?

etam1024
источник
3
Мой G ++ 4.7.3 говорит internal compiler error: Segmentation faultэтому коду ...
Фред Фу
2
(ошибка C2864: 'A :: B :: i': в классе могут быть инициализированы только интегральные элементы данных static const) - это то, что говорит VC2010. Этот вывод согласуется с g ++. Кланг тоже так говорит, хотя в этом гораздо меньше смысла. Вы не можете использовать переменную в структуре по умолчанию, int i = 0если это не так static const int i = 0.
Крис Купер
@Borgleader: Кстати, я бы избегал соблазна думать о выражении B()как о вызове функции конструктора. Вы никогда не «вызываете» конструктор напрямую. Подумайте об этом как о специальном синтаксисе, который создает временный B... и конструктор вызывается как одна часть этого процесса, глубоко внутри следующего механизма.
Гонки легкости на орбите
2
Хм, добавление конструктора, Bпохоже, заставляет эту работу работать gcc 4.7.
Shafik Yaghmour 02
7
Интересно, что перенос определения конструктора A из A также, кажется, заставляет его работать (g ++ 4.7); какие звонки с "конструктором по умолчанию не могут быть использованы ... до конца определения класса".
moonshadow 02 июл.13,

Ответы:

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 для решения этой проблемы. Правило Clang заключается в том, что конструктор по умолчанию по умолчанию для класса не может использоваться до того, как будут проанализированы инициализаторы нестатических элементов данных для этого класса. Следовательно, Clang выдает здесь диагностику:

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

... потому что Clang анализирует аргументы по умолчанию до того, как он анализирует инициализаторы по умолчанию, и этот аргумент по умолчанию потребует, чтобы Bинициализаторы по умолчанию уже были проанализированы (для неявного определения B::B()).

Ричард Смит
источник
Хорошо знать. Но сообщение об ошибке по-прежнему вводит в заблуждение, поскольку конструктор на самом деле не «используется инициализатором нестатических элементов данных».
aschepler 02
Знаете ли вы это из-за конкретного прошлого опыта решения этой проблемы или просто внимательно прочитав стандарт (и список дефектов)? Также +1.
Cornstalks
+1 за подробный ответ. Так какой же выход? Какой-то «двухэтапный синтаксический анализ классов», при котором синтаксический анализ внешних членов класса, зависящих от внутренних классов, откладывается до тех пор, пока внутренние классы не будут полностью сформированы?
TemplateRex
4
@aschepler Да, здесь диагностика не очень хорошая. Я подал для этого файл llvm.org/PR16550.
Ричард Смит
@Cornstalks Я обнаружил эту проблему при реализации инициализаторов для нестатических элементов данных в Clang.
Ричард Смит
0

Может в этом проблема:

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

Таким образом, конструктор по умолчанию создается при первом поиске, но поиск завершится неудачно, потому что A не полностью определен, и поэтому B внутри A не будет найден.

fscan
источник
Я не уверен насчет этого «поэтому». Ясно, что B bэто не проблема, и найти явные методы / явно объявленный конструктор в Bне проблема. Так что было бы неплохо увидеть какое-то определение того, почему поиск здесь должен происходить по-другому, чтобы « Bвнутри Aне было найдено» только в этом одном случае, но не в других, прежде чем мы сможем объявить код незаконным по этой причине.
moonshadow 02 июл.13,
Я нашел в стандарте слова о том, что определение класса считается завершенным во время инициализации внутри класса, в том числе внутри вложенных классов. Я не стал записывать ссылку, потому что она не казалась актуальной.
Mark B
@moonshadow: в заявлении говорится, что неявно заданные по умолчанию конструкторы определяются при использовании odr. явно определяется после первого объявления. И B b не вызывает конструктор, конструктор A вызывает конструктор B
fscan 02
Если бы что-то подобное было проблемой, код все равно был бы недействительным, если вы удалите =0из i = 0;. Но без этого =0код действителен, и вы не найдете ни одного компилятора, который жалуется на использование в B()пределах определения A.
aschepler 02