Всегда ли конструктор по умолчанию инициализирует всех членов?

Я мог бы поклясться, что не помню, чтобы я видел это раньше, и мне трудно поверить в мои глаза:

Является ли неявно определенный конструктор по умолчанию для неагрегатного класса инициализировать его члены или нет?

В Visual С++, когда я запускаю этот невидимый код...

#include <string>
struct S { int a; std::string b; };
int main() { return S().a; }

... к моему удивлению, он возвращает ненулевое значение! Но если я удаляю поле b, то он возвращает ноль.

Я пробовал это во всех версиях VС++, я могу взять себя в руки, и, похоже, все это делает.

Но когда я пытаюсь сделать это на Clang и GCC, значения инициализируются до нуля, пытаюсь ли я его использовать в режиме С++ 98 или С++ 11.

Какое правильное поведение? Не гарантируется ли оно равным нулю?

Ответы

Ответ 1

Цитата С++ 11:

5.2.3 Явное преобразование типа (функциональная нотация) [expr.type.conv]

2 Выражение T(), где T является спецификатором простого типа или спецификатором имени для типа объекта без массива или типа (возможно, cv-qualit) void, создает prvalue указанный тип, который инициализируется значением (8.5; инициализация не выполняется для случая void()). [...]

8.5 Инициализаторы [dcl.init]

7 Для инициализации значения объекта типа T означает:

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

Итак, в С++ 11 S().a должен быть равен нулю: объект инициализируется нулем перед вызовом конструктора, и конструктор никогда не изменяет значение a на что-либо еще.

До С++ 11 инициализация значения имела другое описание. Цитирование N1577 (примерно С++ 03):

Для инициализации объекта типа типа T означает:

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

Здесь инициализация значения S не вызывала никакого конструктора, но вызывала инициализацию значения ее членов a и b. Инициализация значения этого члена a затем вызвала нулевую инициализацию этого конкретного члена. В С++ 03 результат также гарантированно равен нулю.

Еще раньше, перейдя к самому первому стандарту, С++ 98:

Выражение T(), где T является спецификатором простого типа (7.1.5.2) для типа объекта без массива или типа (возможно, cv-qualified) void, создает rvalue указанный тип, значение которого определяется инициализацией по умолчанию (8.5; инициализация не выполняется для случая void()).

Для инициализации объекта типа T по умолчанию:

  • Если T - тип класса не-POD (раздел 9), вызывается конструктор по умолчанию для T (и инициализация плохо сформирована, если T не имеет доступного конструктора по умолчанию);
  • ...
  • в противном случае хранилище для объекта инициализируется нулем.

Итак, на основе этого самого первого стандарта, VС++ корректен: когда вы добавляете член std::string, S становится не-POD-типом, а не-POD-типы не получают нулевой инициализации, они просто имеют свои конструктор называется. Неявно созданный конструктор по умолчанию для S не инициализирует член a.

Таким образом, все компиляторы можно считать правильными, просто следуя различным версиям стандарта.

Как сообщается @Columbo в комментариях, более поздние версии VС++ приводят к инициализации элемента a в соответствии с более поздними версиями стандарта С++.

Ответ 2

(Все кавычки в первом разделе относятся к N3337, С++ 11 FD с редакционными изменениями)

Я не могу воспроизвести поведение с VС++ в реестре. Предположительно ошибка (см. Ниже) уже исправлена ​​в версии, которую они используют, но не в вашем - @Drop сообщает, что последняя версия VS 2013 Update 4 завершает утверждение - в то время как предварительный просмотр VS 2015 передает их.

Только во избежание недоразумений: S действительно является совокупностью. [Dcl.init.aggr]/1:

Агрегат - это массив или класс (раздел 9) без каких-либо пользовательских конструкторы (12.1), частные или защищенные нестатические элементы данных (Раздел 11), нет базовых классов (раздел 10) и нет виртуальных функций (10.3).

Это не имеет значения. Семантика инициализации значений важна. [Dcl.init]/11:

Объект, инициализатор которого представляет собой пустой набор скобок, т.е. (), инициализируется значением.

[dcl.init]/8:

Для инициализации значения объекта типа T означает:

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

Ясно, что это выполняется независимо от того, находится ли b в S или нет. Таким образом, по крайней мере, в С++ 11 в обоих случаях a должен быть равен нулю. Clang и GCC показывают правильное поведение.


А теперь посмотрим на С++ 03 FD:

Для инициализации значения объекта типа T означает:

  • если T - тип класса (раздел 9) с конструктором, объявленным пользователем (12.1) [..]
  • , если T - это неединичный класс класса без объявленного пользователем конструктора, то каждый нестатический член данных и базовый класс компонент T инициализируется значением;
  • Если T - тип массива, то каждый элемент инициализируется значением;
  • , иначе объект инициализируется нулем

То есть, даже в С++ 03 (где приведенная выше цитата в [dcl.init]/11 также существует в /7), a должен быть 0 в обоих случаях. Опять же, как GCC, так и Clang верны с -std = С++ 03.

Как показано в hvd answer, ваша версия совместима только для С++ 98 и С++ 98.