Почему член в этом примере не инициализируется нулями?
Это особенно касается С++ 11:
#include <iostream>
struct A {
A(){}
int i;
};
struct B : public A {
int j;
};
int main() {
B b = {};
std::cout << b.i << b.j << std::endl;
}
Компиляция с g++ 8.2.1:
$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main():
a.cpp:25:25: warning: ‘b.B::<anonymous>.A::i is used uninitialized in this function [-Wuninitialized]
std::cout << b.i << " " << b.j << std::endl
gcc определяет bi
как неинициализированный, но я думаю, что он должен инициализироваться нулем вместе с bj
.
То, что я считаю, происходит (в частности, С++ 11, из рабочего проекта ISO/IEC N3337, выделено мной):
-
B
не является агрегатом, поскольку имеет базовый класс. Публичные базовые классы были разрешены только в агрегатах в С++ 17. -
A
не является агрегатом, потому что имеет пользовательский конструктор
Раздел 8.5.1
Агрегат - это массив или класс (раздел 9) без предоставленных пользователем конструкторов (12.1), без инициалов-скобок для не статических элементов данных (9.2), без закрытых или защищенных нестатических элементов данных (раздел 11), нет базовых классов (пункт 10) и нет виртуальных функций (10.3).
-
b
получает список, инициализированный пустым списком фигурных скобок
Раздел 8.5.4
Инициализация списка объекта или ссылки типа T определяется следующим образом:
- Если список инициализаторов не имеет элементов и T является типом класса с конструктором по умолчанию, объект инициализируется значением.
- В противном случае, если T является агрегатом, выполняется агрегатная инициализация (8.5.1).
- Это означает, что
b
получает значение инициализировано -
B
имеет неявно определенный конструктор по умолчанию, поэтому инициализация значения b
вызывает инициализацию с нуля -
bB::A
инициализируется нулями, что инициализирует нулями bB::Ai
, а затем bB::j
инициализируется нулями.
Раздел 8.5
Инициализация с нуля объекта или ссылки типа T означает:
...
- если T является (возможно, cv-квалифицированным) типом несоединения классов, каждый элемент нестатических данных и каждый подобъект базового класса инициализируются нулями, а заполнение инициализируется нулевыми битами;
...
Инициализировать значение объекта типа T означает:
- если T является (возможно, cv-квалифицированным) типом класса (раздел 9) с предоставленным пользователем конструктором (12.1), то вызывается конструктор по умолчанию для T (и инициализация является некорректной, если у T нет доступного конструктора по умолчанию) );
- если T является (возможно, cv-квалифицированным) типом класса, не являющимся объединением, без предоставленного пользователем конструктора, то объект инициализируется нулями и, если неявно объявленный Ts конструктор по умолчанию является нетривиальным, вызывается этот конструктор.
Тем не менее, похоже, что gcc говорит, что только bB::j
будет инициализироваться нулями. Почему это?
Одна причина, о которой я могу подумать, заключается в том, что B
обрабатывается как агрегат, который инициализирует bB::A
пустым списком. B
, конечно, не является агрегатом, потому что gcc справедливо ошибается, если мы пытаемся использовать агрегатную инициализацию.
// ... as in the above example
int main() {
B b = {A{}, 1};
std::cout << b.i << " " << b.j << std::endl;
}
Компиляция с С++ 11
$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main():
a.cpp:10:18: error: could not convert ‘{A(), 1} from ‘<brace-enclosed initializer list> to ‘B
B b = {A{}, 1};
Компиляция с С++ 17
g++ -std=c++17 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main():
a.cpp:11:25: warning: ‘b.B::<anonymous>.A::i is used uninitialized in this function [-Wuninitialized]
std::cout << b.i << " " << b.j << std::endl;
И мы можем видеть, что bi
неинициализирован, потому что B
является агрегатом, а bB::A
инициализируется выражением, которое само оставляет A::i
неинициализированным.
Так что это не совокупность. Другая причина в том, что bB::j
инициализируется нулем, а bB::A
инициализируется значением, но я не вижу этого нигде в спецификации.
Последняя причина в том, что была вызвана более старая версия стандарта. Из контекста:
2) если T является типом класса, не являющимся объединением, без каких-либо предоставленных пользователем конструкторов, каждый элемент нестатических данных и компонент базового класса T инициализируется значением; (до С++ 11)
В этом случае и bB::i
и bB::A
будут инициализированы значением, что приведет к такому поведению, но будет помечено как "(до С++ 11)".
Ответы
Ответ 1
Я также пошел бы с ошибкой компилятора.
- Я думаю, что мы все можем согласиться с тем, что
b
получает значение инициализировано (8.5.4) -
С помощью
Значение-инициализация объекта типа T означает:
- если T является (возможно, cv-квалифицированным) типом класса, не являющимся объединением, без предоставленного пользователем конструктора, то объект инициализируется нулями и, если неявно объявленный Ts конструктор по умолчанию является нетривиальным, вызывается этот конструктор.
Итак, что должно произойти, это сначала инициализация нуля, затем могут быть вызваны ctors по умолчанию
- И с определением:
Инициализация с нуля объекта или ссылки типа T означает:
- если T является (возможно, cv-квалифицированным) типом несоединения классов, каждый элемент нестатических данных и каждый подобъект базового класса инициализируются нулями, а заполнение инициализируется нулевыми битами;
Следовательно, должно произойти следующее:
- Заполните
sizeof(B)
с нулями - Вызвать конструктор подобъекта
A
который ничего не делает.
Я предполагаю, что это ошибка в оптимизации. Сравните выходные данные -O0
с -O1
: https://godbolt.org/z/20QBoR. Без оптимизации поведение будет правильным. Сланг, с другой стороны, правильный в обоих: https://godbolt.org/z/7uhlIi
Эта "ошибка" все еще присутствует с новыми стандартными флагами в GCC: https://godbolt.org/z/ivkE5K
Однако я предполагаю, что в С++ 20 B
является "агрегатом", поэтому поведение становится стандартным.
Ответ 2
Для любого класса, если существует один определяемый пользователем конструктор, он должен использоваться, и A(){}
не инициализирует i
.
Ответ 3
Ничто не инициализирует i
. Это не происходит автоматически. Вам нужно либо инициализировать его в классе, либо в списке инициализации конструктора класса. Или удалите ваш нетривиальный/определяемый пользователем конструктор (или = default
его по = default
, что делает его тривиальным).
Компилятор использует предоставленный вами конструктор, и этот ctor не инициализирует i
.