Инициализация нуля в С++ - почему в этой программе не инициализируется "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, инициализация по умолчанию - инициализация нуля.

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

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

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

  3. В С++ 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 определяется следующим образом:

  • ...

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

От Витторио Ромео ответ

Ответ 3

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

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

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

[Надрез]

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

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

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

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

Учитывая это определение, 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 инициализируются здесь с неопределенным значением.