Почему в этом примере член не инициализируется нулем?

Это касается конкретно C ++ 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 определяет b.i как неинициализированный, но я думаю, он должен получить нулевую инициализацию вместе с b.j.

То, что я верю, происходит (в частности, C ++ 11, из рабочего проекта ISO / IEC N3337, выделено мной):

  • B не является агрегатом, потому что у него есть базовый класс. Общедоступные базовые классы были разрешены только в агрегатах в C ++ 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 инициализация значения вызывает нулевую инициализацию
  • b.B::A инициализируется нулем, который инициализирует b.B::A.i нулем, а затем b.B::j инициализируется нулем.

Раздел 8.5

Чтобы инициализировать нулевой объект или ссылку типа T означает:
...
- если T является (возможно, cv-квалифицированным) типом класса без объединения, каждый нестатический элемент данных и каждый подобъект базового класса инициализируется нулем, а заполнение инициализируется нулевыми битами;

...

Инициализация значения объекта типа T означает:
- если T является типом класса (возможно cv-квалифицированным) (раздел 9) с конструктором, предоставленным пользователем (12.1), то вызывается конструктор по умолчанию для T ( и инициализация неправильно сформирована, если у T нет доступного конструктора по умолчанию);
- если T является (возможно, cv-квалифицированным) типом класса без объединения без конструктора, предоставленного пользователем, то объект равен нулю -initialized и, если неявно объявленный конструктор T по умолчанию нетривиален, этот конструктор вызывается.

Однако похоже, что gcc говорит, что только b.B::j получит нулевую инициализацию. Почему это?

Одна из причин, о которой я могу думать, - это если B рассматривается как агрегат, который инициализирует b.B::A пустым списком. B, конечно, не является агрегатом, потому что gcc правильно ошибается, если мы пытаемся использовать агрегатную инициализацию.

// ... as in the above example
int main() {
    B b = {A{}, 1};
    std::cout << b.i << " " << b.j << std::endl;
}

Компиляция с C ++ 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};

Компиляция с C ++ 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;

И мы видим, что b.i не инициализирован, потому что B является агрегатом, а b.B::A инициализируется выражением, которое само по себе оставляет A::i неинициализированным.

Так что это не совокупность. Другая причина в том, что b.B::j инициализируется нулем, а b.B::A инициализируется значением, но я не вижу этого нигде в спецификациях.

Последняя причина в том, что вызывалась более старая версия стандарта. Из cppreference:

2) если T является типом класса без объединения без каких-либо предоставленных пользователем конструкторов, каждый нестатический член данных и компонент базового класса T инициализируется значением; (до C ++ 11)

В этом случае и b.B::i, и b.B::A будут инициализированы значением, что вызовет такое поведение, но оно помечено как "(до C ++ 11)".


person Mike Lui    schedule 03.01.2019    source источник
comment
Вы определили конструктор, и компилятор его использует. A() = default; Или просто не определяйте его вообще, поскольку у вас нет членов пользовательских типов.   -  person Koori    schedule 03.01.2019
comment
@Koori b получает нулевую инициализацию. Это должно обнулить все базовые и нестатические члены. Если только он не инициализируется нулем ...   -  person Mike Lui    schedule 03.01.2019
comment
Проблема в том, что компилятор не инициализирует базы нулями, он фактически вызывает его конструктор по умолчанию. Поскольку вы определили конструктор для A, конструктор B вызовет этот конструктор A, который вы определили. Вам просто нужно просто удалить A(){} или изменить его на A() = default;   -  person Koori    schedule 03.01.2019
comment
@MikeLui Если вы хотите, чтобы ответ содержал ссылки из стандарта для их поддержки, вы должны добавить language-lawyer ярлык.   -  person NathanOliver    schedule 03.01.2019
comment
Ошибка компилятора. (Но в любом случае полагаться на это, вероятно, излишне хрупко.)   -  person T.C.    schedule 04.01.2019


Ответы (3)


Я бы также пошел с ошибкой компилятора.

  • Я думаю, мы все можем согласиться с тем, что b инициализируется значением (8.5.4)
  • # P2 #
    # P3 #
    # P4 #
  • И с определением:
    # P5 #

Следовательно, должно произойти следующее:

  1. Заполните sizeof(B) нулями
  2. Вызвать конструктор подобъекта A, который ничего не делает.

Полагаю, это ошибка оптимизации. Сравните вывод -O0 с -O1: https://godbolt.org/z/20QBoR. Без оптимизации поведение правильное. С другой стороны, Clang верен в обоих: https://godbolt.org/z/7uhlIi

Эта «ошибка» все еще присутствует в новых стандартных флагах в GCC: https://godbolt.org/z/ivkE5K

Однако я предполагаю, что C ++ 20 B является «агрегатом», поэтому поведение становится стандартным.

person Flamefire    schedule 07.01.2019

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

person Community    schedule 03.01.2019
comment
Всегда есть конструктор. В зависимости от того, как он определен (предоставлен пользователем или нет), объект может быть инициализирован нулем заранее. Я считаю, что в этой ситуации должна быть нулевая инициализация. Наличие конструктора в базовом классе не препятствует инициализации нуля, как я видел в стандарте. Рабочий черновик C ++ 11 8.5: Для инициализации объекта или ссылки типа T нулем означает: ... если T является (возможно, cv-квалифицированным) типом класса без объединения, каждый нестатический член данных и каждый подобъект базового класса инициализируется нулем, а заполнение инициализируется нулевыми битами; - person Mike Lui; 03.01.2019
comment
Раздел об нулевой инициализации, кажется, не согласен с этим. - person NathanOliver; 03.01.2019
comment
Добавлен пользовательский. - person ; 03.01.2019

Ничего не инициализируется i. Это не происходит автоматически. Вам нужно либо инициализировать его в классе, либо в списке инициализации конструктора класса. Или удалите свой нетривиальный / определяемый пользователем конструктор (или = default его, что делает его тривиальным).

Компилятор использует предоставленный вами конструктор, и этот ctor не инициализирует i.

person Jesper Juhl    schedule 03.01.2019
comment
Или удалите определяемый пользователем конструктор по умолчанию. - person juanchopanza; 03.01.2019