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: попередження: 'b.bar::b' використовується неініціалізованим у цій функції [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl

8
barконструктор 's надається користувачем, тоді як 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]) або конструктора за замовчуванням, який надається користувачем або видаляється, тоді об'єкт ініціалізується за замовчуванням ;


52
Я маю на увазі (*_*).... Якщо навіть використовувати основні конструкції мови, мені потрібно прочитати тонкий шрифт мовної чернетки, то Аллелуя! Але це, мабуть, здається, що ви говорите.
Дак Доджерс

12
@balki Так, робити bar::bar() = defaultпоза лінії - це те саме, що робити bar::bar(){}Inline.
NathanOliver

15
@JoeyMallone Так, C ++ може бути досить складним. Я не впевнений, в чому причина цього.
NathanOliver

3
Якщо є попереднє оголошення, то наступне визначення з ключовим словом за замовчуванням НЕ нульове ініціалізацію членів. Правильно? Це вірно. Це те, що тут відбувається.
NathanOliver

6
Причина саме у вашій цитаті: суть нестандартного за замовчуванням полягає в тому, щоб "забезпечити ефективне виконання та стисле визначення при одночасному включенні стабільного бінарного інтерфейсу до кодової бази, що розвивається", іншими словами, дайте можливість перейти на пізніше написане користувачем тіло, якщо необхідно, без порушення ABI. Зауважте, що позалінійне визначення не є імпліцитно вбудованим, і тому може з'являтися лише в одному TU за замовчуванням; інший TU, що бачить лише визначення класу, не має змоги дізнатися, чи він чітко визначений як дефолт.
ТК

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

З cppreference :

Агрегатна ініціалізація ініціалізує агрегати. Це форма ініціалізації списку.

Агрегат є одним із таких типів:

[сніп]

  • тип класу [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 разів зараз, a завжди дорівнює нулю. b трохи змінюється.

Те, що значення було однакове кілька разів, не означає, що воно було ініціалізовано.

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

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

Як я міг якось трохи потрусити оперативну пам’ять, щоб якщо там було нуль, то тепер має бути щось інше

У C ++ немає гарантованого способу зробити неініціалізоване значення значення, яке не відображається на нулі.

Єдиний спосіб дізнатися, що ініціалізована змінна - це порівняти програму з правилами мови та переконатися, що правила говорять про її ініціалізацію. У цьому випадку a.aсправді ініціалізується.


"Конструктор за замовчуванням, який ви визначили за замовчуванням, ініціалізує членів (в силу відсутності ініціалізаторів членів), який у випадку int залишає його з невизначеним значенням." -> е-е! "для типів pod, типовою ініціалізацією за замовчуванням є нульова ініціалізація." чи я помиляюся?
Дак Доджерс

2
@JoeyMallone Ініціалізація типів POD за замовчуванням не є ініціалізацією.
NathanOliver

@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 ]

Тож саме там стає цікаво, це чітко показує, що збірка клацання 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
}

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.