Ошибка компиляции при использовании пустого конструктора инициализации списка в С++ 17

Я столкнулся со странной проблемой при попытке перейти на С++ 17. Проблема в том, что кое-что (и я не уверен, что) изменилось в С++ 17, что заставило инициализацию списка работать по-другому в случае конструктора по умолчанию. Я пытался найти https://en.cppreference.com/w/cpp/language/list_initialization для получения дополнительной информации, но я не нашел ничего, что выглядит уместным.

Кто-нибудь знает причину, по которой код ниже компилируется в С++ 14, но не в С++ 17 при вызове B{} вместо B()? (Я попробовал это и в gcc 8.2 и 7.3 и в icc 19)

struct A{
protected:
    A() {}
};

struct B : public A {};


B f(){
    return B(); //compilation OK
    //return B{}; //compilation error
}

Ответ 1

В С++ 14 определение агрегата было:

Агрегат - это массив или класс (Clause [class]) без предоставленных пользователем конструкторов ([class.ctor]), без закрытых или защищенных нестатических элементов данных (Clause [class.access]), без базовых классов ( Предложение [class.derived]), и нет виртуальных функций ([class.virtual]).

Следовательно, B не является агрегатом. В результате B{}, безусловно, не является агрегатной инициализацией, а B{} и B() означают одно и то же. Они оба просто вызывают конструктор B умолчанию.

Однако в С++ 17 определение агрегата было изменено на:

Агрегат - это массив или класс с

  • нет пользовательских, явных или унаследованных конструкторов ([class.ctor]),
  • нет частных или защищенных нестатических членов данных (пункт [class.access]),
  • нет виртуальных функций, и
  • нет виртуальных, частных или защищенных базовых классов ([class.mi]).

[Примечание: Агрегированная инициализация не позволяет получить доступ к защищенным и закрытым членам или конструкторам базового класса. - конец примечания]

Ограничение больше не для каких-либо базовых классов, а только для виртуальных/частных/защищенных. Но у B есть публичный базовый класс. Теперь это совокупность! И агрегатная инициализация С++ 17 позволяет инициализировать подобъекты базового класса.

В частности, B{} является агрегатной инициализацией, где мы просто не предоставляем инициализатор для любого подобъекта. Но первым (и единственным) подобъектом является A, который мы пытаемся инициализировать из {} (во время инициализации агрегата любой подобъект без явного инициализатора инициализируется копией из {}), чего мы не можем сделать, потому что A Конструктор защищен, и мы не друг (см. также цитату).


Обратите внимание, что для интереса в С++ 20 определение агрегата снова изменится.

Ответ 2

Из моего понимания https://en.cppreference.com/w/cpp/language/value_initialization

B{} выполняет агрегирование_инициализации,

а с С++ 17:

Эффекты инициализации агрегата:

  • Каждая прямая общедоступная база (начиная с С++ 17) [..] инициализируется копией из соответствующего предложения списка инициализаторов.

и в нашем случае:

Если количество предложений инициализатора меньше количества членов и баз (начиная с С++ 17) или список инициализаторов полностью пуст, остальные члены и базы (начиная с С++ 17) инициализируются инициализаторами по умолчанию, если они предоставляются в определении класса, и в противном случае (начиная с С++ 14) пустыми списками, в соответствии с обычными правилами инициализации списков (которые выполняют инициализацию значений для не-классов типов и неагрегированных классов с конструкторами по умолчанию, и агрегированной инициализации для агрегатов). Если член ссылочного типа является одним из этих оставшихся членов, программа является некорректной.

Поэтому B{/*constructor of A*/} должен построить базовый класс A, который защищен...

Ответ 3

Окончательный вариант C++ 17 n4659 содержит раздел совместимости, в котором содержатся изменения относительно предыдущих версий.

C.4.4 Пункт 11: деклараторы [diff.cpp14.decl]

11.6.1
Изменение: определение агрегата расширено для применения к пользовательским типам с базовыми классами.
Обоснование: повысить удобство инициализации агрегата.
Влияние на исходную функцию: действительный код C++ 2014 года может не скомпилировать или привести к другим результатам в этом международном стандарте; инициализация из пустого списка инициализаторов выполнит агрегатную инициализацию вместо вызова конструктора по умолчанию для затронутых типов:

struct derived;
struct base {
friend struct derived;
private:
base();
};
struct derived : base {};
derived d1{}; // Error. The code was well-formed before.
derived d2; // still OK

Я скомпилировал приведенный выше пример кода с -std=C++14 и он скомпилирован, но не скомпилирован с -std=C++17.

Я считаю, что это может быть причиной того, что код в OP не работает с B{} но успешно с B().