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