Инициализация нуля в С++ - почему в этой программе не инициализируется "b", а "a" инициализируется?
Согласно принятому (и единственному) ответу на этот вопрос,
Определение конструктора с помощью
MyTest() = default;
вместо этого будет нулевая инициализация объекта.
Тогда почему следующее,
#include <iostream>
struct foo {
foo() = default;
int a;
};
struct bar {
bar();
int b;
};
bar::bar() = default;
int main() {
foo a{};
bar b{};
std::cout << a.a << ' ' << b.b;
}
произвести этот вывод:
0 32766
Оба конструктора определены по умолчанию? Правильно? И для типов POD, инициализация по умолчанию - инициализация нуля.
И согласно принятому ответу на этот вопрос,
-
Если член POD не инициализируется ни в конструкторе, ни через инициализацию в классе С++ 11, он инициализируется по умолчанию.
-
Ответ один и тот же, независимо от стека или кучи.
-
В С++ 98 (а не после) новый int() был указан как выполняющий нулевую инициализацию.
Несмотря на попытки обернуть мою (хотя и крошечную) голову вокруг конструкторов по умолчанию и инициализации по умолчанию, я не смог придумать объяснения.
Ответы
Ответ 1
Проблема здесь довольно тонкая. Вы думаете, что
bar::bar() = default;
даст вам сгенерированный компилятором конструктор по умолчанию, и это так, но теперь он считается предоставленным пользователем. [dcl.fct.def.default]/5 состояний:
Явно-дефолтные функции и неявно-объявленные функции вместе называются дефолтными функциями, и реализация должна предоставлять для них неявные определения ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign) ]), что может означать определение их как удаленных. Функция предоставляется пользователем, если она объявлена пользователем и не имеет явных значений по умолчанию или удалена в первом объявлении. Предоставленная пользователем явно дефолтная функция (т.е. Явно дефолтная после ее первого объявления) определяется в точке, где она явно дефолтна; если такая функция неявно определена как удаленная, программа является некорректной. [Примечание: Объявление функции по умолчанию после ее первого объявления может обеспечить эффективное выполнение и краткое определение при одновременном включении стабильного двоичного интерфейса в развивающуюся базу кода. - конец примечания]
акцент мой
Таким образом, мы можем видеть, что, поскольку вы не использовали по умолчанию bar()
когда вы впервые объявили его, теперь он считается предоставленным пользователем. Из-за этого [dcl.init]/8.2
если T является (возможно, квалифицированным по cv) типом класса без предоставленного пользователем или удаленного конструктора по умолчанию, тогда объект инициализируется нулями, и проверяются семантические ограничения для инициализации по умолчанию, и если T имеет нетривиальный конструктор по умолчанию объект инициализируется по умолчанию;
больше не применяется, и мы не инициализируем значение b
а вместо этого инициализируем его по умолчанию согласно [dcl.init]/8.1
если T является (возможно, cv-квалифицированным) типом класса ([class]) без конструктора по умолчанию ([class.default.ctor]) или конструктора по умолчанию, предоставленного или удаленного пользователем, тогда объект инициализируется по умолчанию;
Ответ 2
Разница в поведении происходит из-за того, что, согласно [dcl.fct.def.default]/5
, bar::bar
предоставляется пользователем, где foo::foo
не равно 1. Как следствие, foo::foo
будет инициализировать значения своих членов (что означает: zero-initialize foo::a
), но bar::bar
останется неинициализированным 2.
1)[dcl.fct.def.default]/5
Функция предоставляется пользователем, если она объявлена пользователем и не имеет явных значений по умолчанию или удалена в первом объявлении.
2)
Из [dcl.init # 6]:
Инициализировать значение объекта типа T означает:
-
если T является (возможно, cv-квалифицированным) типом класса без конструктора по умолчанию ([class.ctor]) или конструктора по умолчанию, предоставленного или удаленного пользователем, тогда объект инициализируется по умолчанию;
-
если T является (возможно, квалифицированным по cv) типом класса без предоставленного пользователем или удаленного конструктора по умолчанию, тогда объект инициализируется нулями, и проверяются семантические ограничения для инициализации по умолчанию, и если T имеет нетривиальный конструктор по умолчанию объект инициализируется по умолчанию;
-
...
Из [dcl.init.list]:
Инициализация списка объекта или ссылки типа T определяется следующим образом:
От Витторио Ромео ответ
Ответ 3
Из контекста:
Инициализация агрегатов инициализирует агрегаты. Это форма инициализации списка.
Агрегат является одним из следующих типов:
[Надрез]
Учитывая это определение, foo
является агрегатом, а bar
- нет (он имеет предоставленный пользователем конструктор, не являющийся дефолтом).
Поэтому для foo
, T object {arg1, arg2,...};
это синтаксис для агрегатной инициализации.
Эффекты инициализации агрегата:
-
[snip] (некоторые детали не имеют отношения к этому делу)
-
Если число предложений инициализатора меньше, чем количество членов, или список инициализаторов полностью пуст, остальные члены инициализируются значением.
Следовательно, aa
является значением, инициализированным, что для int
означает нулевую инициализацию.
Для bar
T object {};
с другой стороны, это инициализация значения (экземпляра класса, а не инициализация значения членов!). Поскольку это тип класса с конструктором по умолчанию, вызывается конструктор по умолчанию. Конструктор по умолчанию, который вы определили по умолчанию, инициализирует элементы (в силу отсутствия инициализаторов элементов), что в случае int
(с нестатическим хранилищем) оставляет bb
с неопределенным значением.
А для типов pod инициализация по умолчанию - инициализация нуля.
Нет, это неправильно
PS Несколько слов о вашем эксперименте и вашем заключении: видение того, что результат равен нулю, не обязательно означает, что переменная была инициализирована нулем. Ноль - это вполне возможное число для значения мусора.
для этого я запускал программу, может быть, 5 ~ 6 раз перед публикацией и примерно 10 раз сейчас, а всегда равно нулю. б немного меняется.
Тот факт, что значение было одним и тем же несколько раз, не обязательно означает, что оно также было инициализировано.
Я также попробовал с набором (CMAKE_CXX_STANDARD 14). Результат был таким же.
Тот факт, что результат одинаков для нескольких опций компилятора, не означает, что переменная инициализирована. (Хотя в некоторых случаях изменение стандартной версии может изменить ее инициализацию).
Как я мог как-то немного потрясти мою оперативную память, чтобы, если там был ноль, теперь было что-то еще?
В C++ нет гарантированного способа сделать так, чтобы значение неинициализированного значения выглядело ненулевым.
Единственный способ узнать, что переменная инициализирована, - это сравнить программу с правилами языка и убедиться, что правила говорят, что она инициализирована. В этом случае aa
действительно инициализируется.
Ответ 4
Я попробовал запустить предоставленный вами фрагмент как test.cpp
, используя gcc & clang и несколько уровней оптимизации:
[email protected] /tmp> g++ -o test.gcc.O0 test.cpp
[ 0s828 | Jan 27 01:16PM ]
[email protected] /tmp> g++ -o test.gcc.O2 -O2 test.cpp
[ 0s901 | Jan 27 01:16PM ]
[email protected] /tmp> g++ -o test.gcc.Os -Os test.cpp
[ 0s875 | Jan 27 01:16PM ]
[email protected] /tmp> ./test.gcc.O0
0 32764 [ 0s004 | Jan 27 01:16PM ]
[email protected] /tmp> ./test.gcc.O2
0 0 [ 0s004 | Jan 27 01:16PM ]
[email protected] /tmp> ./test.gcc.Os
0 0 [ 0s003 | Jan 27 01:16PM ]
[email protected] /tmp> clang++ -o test.clang.O0 test.cpp
[ 1s089 | Jan 27 01:17PM ]
[email protected] /tmp> clang++ -o test.clang.Os -Os test.cpp
[ 1s058 | Jan 27 01:17PM ]
[email protected] /tmp> clang++ -o test.clang.O2 -O2 test.cpp
[ 1s109 | Jan 27 01:17PM ]
[email protected] /tmp> ./test.clang.O0
0 274247888 [ 0s004 | Jan 27 01:17PM ]
[email protected] /tmp> ./test.clang.Os
0 0 [ 0s004 | Jan 27 01:17PM ]
[email protected] /tmp> ./test.clang.O2
0 0 [ 0s004 | Jan 27 01:17PM ]
[email protected] /tmp> ./test.clang.O0
0 2127532240 [ 0s002 | Jan 27 01:18PM ]
[email protected] /tmp> ./test.clang.O0
0 344211664 [ 0s004 | Jan 27 01:18PM ]
[email protected] /tmp> ./test.clang.O0
0 1694408912 [ 0s004 | Jan 27 01:18PM ]
Так что вот где это становится интересным, это ясно показывает, что сборка clang O0 читает случайные числа, предположительно, стекового пространства.
Я быстро включил мою IDA, чтобы посмотреть, что происходит:
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v4; // rax
int result; // eax
unsigned int v6; // [rsp+8h] [rbp-18h]
unsigned int v7; // [rsp+10h] [rbp-10h]
unsigned __int64 v8; // [rsp+18h] [rbp-8h]
v8 = __readfsqword(0x28u); // alloca of 0x28
v7 = 0; // this is foo a{}
bar::bar((bar *)&v6); // this is bar b{}
v3 = std::ostream::operator<<(&std::cout, v7); // this is clearly 0
v4 = std::operator<<<std::char_traits<char>>(v3, 32LL); // 32 = 0x20 = ' '
result = std::ostream::operator<<(v4, v6); // joined as cout << a.a << ' ' << b.b, so this is reading random values!!
if ( __readfsqword(0x28u) == v8 ) // stack align check
result = 0;
return result;
}
Теперь, что делает bar::bar(bar *this)
?
void __fastcall bar::bar(bar *this)
{
;
}
Хм, ничего. Пришлось прибегнуть к использованию сборки:
.text:00000000000011D0 ; __int64 __fastcall bar::bar(bar *__hidden this)
.text:00000000000011D0 public _ZN3barC2Ev
.text:00000000000011D0 _ZN3barC2Ev proc near ; CODE XREF: main+20↓p
.text:00000000000011D0
.text:00000000000011D0 var_8 = qword ptr -8
.text:00000000000011D0
.text:00000000000011D0 ; __unwind {
.text:00000000000011D0 55 push rbp
.text:00000000000011D1 48 89 E5 mov rbp, rsp
.text:00000000000011D4 48 89 7D F8 mov [rbp+var_8], rdi
.text:00000000000011D8 5D pop rbp
.text:00000000000011D9 C3 retn
.text:00000000000011D9 ; } // starts at 11D0
.text:00000000000011D9 _ZN3barC2Ev endp
Так что да, просто, ничего, то, что в основном делает конструктор, this = this
. Но мы знаем, что он на самом деле загружает случайные неинициализированные адреса стека и печатает его.
Что если мы явно предоставим значения для двух структур?
#include <iostream>
struct foo {
foo() = default;
int a;
};
struct bar {
bar();
int b;
};
bar::bar() = default;
int main() {
foo a{0};
bar b{0};
std::cout << a.a << ' ' << b.b;
}
Хит лязг, упс
[email protected] /tmp> clang++ -o test.clang.O0 test.cpp
test.cpp:17:9: error: no matching constructor for initialization of 'bar'
bar b{0};
^~~~
test.cpp:8:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion
from 'int' to 'const bar' for 1st argument
struct bar {
^
test.cpp:8:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion
from 'int' to 'bar' for 1st argument
struct bar {
^
test.cpp:13:6: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
bar::bar() = default;
^
1 error generated.
[ 0s930 | Jan 27 01:35PM ]
Аналогичная судьба и с g++:
[email protected] /tmp> g++ test.cpp
test.cpp: In function ‘int main():
test.cpp:17:12: error: no matching function for call to ‘bar::bar(<brace-enclosed initializer list>)
bar b{0};
^
test.cpp:8:8: note: candidate: ‘bar::bar()
struct bar {
^~~
test.cpp:8:8: note: candidate expects 0 arguments, 1 provided
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(const bar&)
test.cpp:8:8: note: no known conversion for argument 1 from ‘int to ‘const bar&
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(bar&&)
test.cpp:8:8: note: no known conversion for argument 1 from ‘int to ‘bar&&
[ 0s718 | Jan 27 01:35PM ]
Таким образом, это означает, что это эффективная bar b(0)
прямой инициализации bar b(0)
, а не совокупная инициализация.
Вероятно, это связано с тем, что если вы не предоставите явную реализацию конструктора, это может быть внешний символ, например:
bar::bar() {
this.b = 1337; // whoa
}
Компилятор не достаточно умен, чтобы выводить это как неактивный/встроенный вызов на неоптимизированной стадии.
Ответ 5
Поведение по умолчанию конструктора по умолчанию
он имеет тот же эффект, что и пользовательский конструктор с пустым телом и пустым списком инициализатора. То есть он вызывает конструкторы по умолчанию для баз и нестатических членов этого класса.
Это означает, что для foo
и bar
их конструктор по умолчанию по умолчанию будет инициализировать их нестатические члены-данные по умолчанию. И тогда эффект будет
в противном случае ничего не делается: объекты с автоматической продолжительностью хранения (и их подобъекты) инициализируются неопределенными значениями.
Это означает, что и aa
и bb
инициализируются здесь с неопределенным значением.