Инициализация нуля в C ++ - почему `b` в этой программе не инициализирован, а` a` инициализирован?

135

Согласно принятому (и единственному) ответу на этот вопрос переполнения стека ,

Определение конструктора с помощью

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 инициализация по умолчанию - инициализация нуля.

И согласно принятому ответу на этот вопрос ,

  1. Если член POD не инициализируется ни в конструкторе, ни через инициализацию в классе C ++ 11, он инициализируется по умолчанию.

  2. Ответ один и тот же, независимо от стека или кучи.

  3. В C ++ 98 (а не после) новый int () был указан как выполняющий нулевую инициализацию.

Несмотря на попытки обернуть мою (хотя и крошечную ) голову вокруг конструкторов по умолчанию и инициализации по умолчанию , я не смог придумать объяснения.

Утка доджерс
источник
3
Интересно, что я даже получаю предупреждение для b: main.cpp: 18: 34: warning: 'b.bar::b' используется неинициализированным в этой функции [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl
8
barКонструктор предоставлен пользователем, а fooконструктор по умолчанию.
Jarod42
2
@PeteBecker, я это понимаю. Как я мог как-то немного потрясти мою оперативную память, чтобы, если там был ноль, теперь было что-то другое. ;) ps я запускал программу десяток раз. Это не большая программа. Вы можете запустить его и протестировать в своей системе. aэто ноль. bне является. Кажется aинициализировано.
Утка Доджерс
2
@JoeyMallone Относительно того, «как это предоставлено пользователем»: нет гарантии, что определение bar::bar()видимо в main()- оно может быть определено в отдельном модуле компиляции и делать что-то очень нетривиальное, в то время как main()только объявление является видимым. Я думаю, вы согласитесь, что это поведение не должно меняться в зависимости от того, помещаете ли вы bar::bar()определение в отдельную единицу компиляции или нет (даже если вся ситуация не интуитивна).
Макс
2
@balki Или int a = 0;ты хочешь быть откровенным?
Натан Оливер

Ответы:

109

Проблема здесь довольно тонкая. Вы думаете, что

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]) или конструктора по умолчанию, предоставленного или удаленного пользователем, тогда объект инициализируется по умолчанию ;

NathanOliver
источник
52
Я имею в виду (*_*).... Если даже использовать базовые конструкции языка, мне нужно прочитать мелкий шрифт проекта языка, тогда Аллилуйя! Но, вероятно, это то, что вы говорите.
Утка Доджерс
12
@balki Да, работа bar::bar() = defaultвне строки - это то же самое , что и работа в строке bar::bar(){}.
Натан Оливер
15
@JoeyMallone Да, C ++ может быть довольно сложным. Я не уверен, в чем причина.
Натан Оливер
3
Если есть предыдущая декларация, то последующее определение с ключевым словом по умолчанию НЕ будет инициализировать элементы нулями. Правильно? Это верно. Это то, что здесь происходит.
Натан Оливер
6
Причина прямо в вашей цитате: смысл внепланового значения по умолчанию состоит в том, чтобы «обеспечить эффективное выполнение и точное определение, в то же время обеспечивая стабильный двоичный интерфейс для развивающейся кодовой базы», ​​другими словами, вы можете переключиться на пользовательское тело позже, если необходимо, без нарушения ABI. Обратите внимание, что внешнее определение не является неявно встроенным и поэтому может отображаться только в одном TU по умолчанию; другой TU, видящий только определение класса, не может узнать, явно ли он определен как дефолтный.
TC
25

Различие в поведении исходит из того , что, по мнению [dcl.fct.def.default]/5, bar::barявляется пользователем при условии , где foo::fooнет 1 . Как следствие, foo::fooбудет инициализировать значения своих членов (что означает: инициализация нуля 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 определяется следующим образом:

  • ...

  • В противном случае, если список инициализаторов не имеет элементов и T является типом класса с конструктором по умолчанию, объект инициализируется значением.

Из ответа Витторио Ромео

МКЦ
источник
10

Из контекста :

Инициализация агрегатов инициализирует агрегаты. Это форма инициализации списка.

Агрегат является одним из следующих типов:

[Надрез]

  • тип класса [snip], который имеет

    • [snip] (есть варианты для разных стандартных версий)

    • нет пользовательских, унаследованных или явных конструкторов (явно дефолтные или удаленные конструкторы допускаются)

    • [snip] (есть больше правил, которые применяются к обоим классам)

Учитывая это определение, fooявляется агрегатом, а barне является (он имеет предоставленный пользователем, не дефолтный конструктор).

Поэтому для foo, T object {arg1, arg2, ...};является синтаксис для агрегатной инициализации.

Последствия инициализации агрегата:

  • [snip] (некоторые детали не имеют отношения к этому делу)

  • Если количество предложений инициализатора меньше, чем количество членов, или список инициализаторов полностью пуст, остальные члены инициализируются значением .

Следовательно a.a, значение инициализируется, что intозначает нулевую инициализацию.

Ибо bar, T object {};с другой стороны, это инициализация значения (экземпляра класса, а не инициализация значения членов!). Поскольку это тип класса с конструктором по умолчанию, вызывается конструктор по умолчанию. Конструктор по умолчанию, который вы определили по умолчанию, инициализирует элементы (из-за отсутствия инициализаторов элементов), который в случае int(с нестатическим хранилищем) выходит b.bс неопределенным значением.

И для типов pod инициализация по умолчанию - инициализация нуля.

Нет, это неправильно


PS Несколько слов о вашем эксперименте и вашем заключении: видение того, что результат равен нулю, не обязательно означает, что переменная была инициализирована нулем. Ноль - это вполне возможное число для значения мусора.

для этого я запускал программу, может быть, 5 ~ 6 раз перед публикацией и примерно 10 раз сейчас, а всегда равно нулю. б немного меняется.

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

Я также попробовал с набором (CMAKE_CXX_STANDARD 14). Результат был таким же.

Тот факт, что результат одинаков для нескольких опций компилятора, не означает, что переменная инициализирована. (Хотя в некоторых случаях изменение стандартной версии может изменить ее инициализацию).

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

В C ++ не существует гарантированного способа сделать так, чтобы значение неинициализированного значения выглядело ненулевым.

Единственный способ узнать, что переменная инициализирована, - это сравнить программу с правилами языка и убедиться, что правила говорят, что она инициализирована. В этом случае a.aдействительно инициализируется.

eerorika
источник
«Конструктор по умолчанию, который вы определили по умолчанию, инициализирует элементы (из-за отсутствия инициализаторов элементов), который в случае int оставляет его с неопределенным значением». -> Эх! "для типов pod инициализация по умолчанию - инициализация нуля." или я не прав?
Утка Доджерс
2
@JoeyMallone Инициализация по умолчанию для типов POD - это не инициализация.
Натан Оливер
@NathanOliver, тогда я еще больше запутался. Тогда как происходит aинициализация. Я думал, aчто инициализация по умолчанию и инициализация по умолчанию для члена POD, нулевая инициализация. Не aто просто , к счастью , всегда придумывает нуль, независимо от того , сколько раз я запускаю эту программу.
Утка Доджерс
@JoeyMallone, Then how come a is initialized.потому что это значение инициализировано. I was thinking a is default initializedЭто не.
eerorika
3
@JoeyMallone Не беспокойся об этом. Вы можете сделать книгу из инициализации в C ++. Если у вас есть шанс, у CppCon на youtube есть несколько видеороликов об инициализации, и самым разочаровывающим (как, например, указанием, насколько это плохо) является youtube.com/watch?v=7DTlWPgX6zs
NathanOliver
0

Я попытался запустить предоставленный вами фрагмент test.cppчерез gcc & clang и несколько уровней оптимизации:

steve@steve-pc /tmp> g++ -o test.gcc.O0 test.cpp
                                                                              [ 0s828 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.O2 -O2 test.cpp
                                                                              [ 0s901 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.Os -Os test.cpp
                                                                              [ 0s875 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O0
0 32764                                                                       [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O2
0 0                                                                           [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.Os
0 0                                                                           [ 0s003 | Jan 27 01:16PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
                                                                              [ 1s089 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.Os -Os test.cpp
                                                                              [ 1s058 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O2 -O2 test.cpp
                                                                              [ 1s109 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 274247888                                                                   [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.Os
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O2
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 2127532240                                                                  [ 0s002 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 344211664                                                                   [ 0s004 | Jan 27 01:18PM ]
steve@steve-pc /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+20p
.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;
}

Хит лязг, упс

steve@steve-pc /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 ++:

steve@steve-pc /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::bar() {
  this.b = 1337; // whoa
}

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

Стив Фан
источник