Безлад у ініціалізації за замовчуванням, значенням та нулем


88

Я дуже заплутаний щодо значення- та за замовчуванням та нульової ініціалізації. і особливо коли вони застосовують різні стандарти C ++ 03 та C ++ 11C ++ 14 ).

Я цитую і намагаюся викласти справді хорошу відповідь Value- / Default- / Zero- Init C ++ 98 і C ++ 03 тут, щоб зробити це більш загальним, оскільки це допомогло б багатьом користувачам, якби хтось міг допомогти заповнити потрібні прогалини, щоб мати хороший огляд того, що відбувається коли?

Повне розуміння на прикладах у двох словах:

Іноді пам'ять, повернута новим оператором, буде ініціалізована, а іноді це не залежить від того, чи тип, який ви створюєте, - це POD (звичайні старі дані) , чи це клас, який містить елементи POD і використовує сформований компілятором конструктор за замовчуванням.

  • У C ++ 1998 існує 2 типи ініціалізації: нульова та за замовчуванням ініціалізація
  • У C ++ 2003 було додано 3-й тип ініціалізації, значення-ініціалізація .
  • У C ++ 2011 / C ++ 2014 було додано лише ініціалізацію списку, а правила для ініціалізації значення / за замовчуванням / нуль трохи змінені.

Припустимо:

struct A { int m; };                     
struct B { ~B(); int m; };               
struct C { C() : m(){}; ~C(); int m; };  
struct D { D(){}; int m; };             
struct E { E() = default; int m;}; /** only possible in c++11/14 */  
struct F {F(); int m;};  F::F() = default; /** only possible in c++11/14 */

У компіляторі C ++ 98 має відбуватися таке :

  • new A - невизначене значення ( Aдорівнює POD)
  • new A()- нульова ініціалізація
  • new B - конструкція за замовчуванням ( B::mнеініціалізована, Bне є POD)
  • new B()- конструкція за замовчуванням ( B::mнеініціалізована)
  • new C - конструкція за замовчуванням ( C::mнульова ініціалізація, Cне є POD)
  • new C()- конструкція за замовчуванням ( C::mнульова ініціалізація)
  • new D - конструкція за замовчуванням ( D::mнеініціалізована, Dне є POD)
  • new D()- конструкція за замовчуванням? ( D::mнеініціалізовано)

У компіляторі відповідності C ++ 03 все повинно працювати так:

  • new A - невизначене значення ( Aдорівнює POD)
  • new A() - значення-ініціалізація A, що є нульовою ініціалізацією, оскільки це POD.
  • new B - за замовчуванням ініціалізується (залишає B::mнеініціалізованим, Bне є POD)
  • new B() - значення ініціалізується B яке нульовим чином ініціалізує всі поля, оскільки його за замовчуванням ctor створюється компілятором на відміну від визначеного користувачем.
  • new C - за замовчуванням ініціалізується C, що викликає за замовчуванням ctor. ( C::mініціалізується нулем,C не є POD)
  • new C() - значення-ініціалізується C, що викликає за замовчуванням ctor. (C::m нульова ініціалізація)
  • new D - конструкція за замовчуванням ( D::mнеініціалізована,D не є POD)
  • new D() - значення ініціалізує D? , який викликає ctor за замовчуванням ( D::mнеініціалізований)

Курсивні значення та? є невизначеності, будь ласка, допоможіть це виправити :-)

У компіляторі відповідності C ++ 11 речі повинні працювати так:

??? (будь ласка, допоможіть, якщо я почну тут, це все одно піде не так)

У компіляторі відповідності C ++ 14 все повинно працювати так: ??? (будь ласка, допоможіть, якщо я почну тут, це все одно піде не так) (проект на основі відповіді)

  • new A - за замовчуванням ініціалізується A, компілятор gen. ctor, (залишки A::mнеініціалізовані) ( Aє POD)

  • new A() - значення-ініціалізується A, що є нульовою ініціалізацією з 2. точки в [dcl.init] / 8

  • new B - за замовчуванням ініціалізується B, компілятор gen. ctor, (залишки B::mнеініціалізовані) ( Bне є POD)

  • new B() - значення-ініціалізує,B яке нульовим чином ініціалізує всі поля, оскільки його за замовчуванням ctor створюється компілятором на відміну від визначеного користувачем.

  • new C - за замовчуванням ініціалізується C, що викликає за замовчуванням ctor. ( C::mнульова ініціалізація,C не є POD)

  • new C() - значення-ініціалізується C, що викликає за замовчуванням ctor. ( C::mініціалізується нулем)

  • new D - за замовчуванням ініціалізується D( D::mнеініціалізований, Dне є POD)

  • new D() - значення-ініціалізується D, що викликає типовий ctor ( D::mнеініціалізований)

  • new E - за замовчуванням ініціалізується E, що викликає комп. ген. ctor. ( E::mнеініціалізовано, E не є POD)

  • new E() - значення-ініціалізується E, яке нульовим чином ініціалізується Eз 2 точки в [dcl.init] / 8 )

  • new F - за замовчуванням ініціалізується F, що викликає комп. ген. ctor. ( F::mнеініціалізовано, Fне є POD)

  • new F() - значення-ініціалізується F, яке за замовчуванням ініціалізується F з 1. точки в [dcl.init] / 8 ( Fфункція ctor надається користувачем, якщо вона оголошена користувачем, а не є явно встановленою за замовчуванням або видалена в першій декларації. Посилання )



1
Наскільки я можу зрозуміти, у цих прикладах між C ++ 98 і C ++ 03 є лише різниця. Проблема, схоже, описана в N1161 (пізніше переглянуто цей документ) та CWG DR # 178 . Формулювання , необхідні для зміни в C ++ 11 з - за нові функції і нову специфікацію POD, і він знову змінилися в C ++ 14 з - за дефекти в C ++ 11 формулювань, але ефекти в цих випадках не змінилися .
dyp

3
Хоча нудно, struct D { D() {}; int m; };можливо, варто включити його у свій список.
Якк - Адам Неврамон

Відповіді:


24

C ++ 14 визначає ініціалізацію об'єктів, створених за допомогою new[expr.new] / 17 ([expr.new] / 15 в C ++ 11, і примітка тоді була не приміткою, а нормативним текстом):

Нове вираз , яке створює об'єкт типу Tинициализирует цей об'єкт наступним чином :

  • Якщо новий ініціалізатор опущений, об’єкт ініціалізується за замовчуванням (8.5). [ Примітка. Якщо ініціалізація не виконується, об’єкт має невизначене значення. - кінцева примітка ]
  • В іншому випадку новий ініціалізатор інтерпретується відповідно до правил ініціалізації 8.5 для прямої ініціалізації .

Ініціалізація за замовчуванням визначена в [dcl.init] / 7 (/ 6 у C ++ 11, і саме формулювання має той самий ефект):

Щоб по замовчуванням форматувати об'єкт типу Tзасобів:

  • якщо Tє типом класу (можливо, має кваліфікацію cv) (пункт 9), Tвикликається конструктор за замовчуванням (12.1) для (і ініціалізація неправильно сформована, якщоT не має конструктора за замовчуванням або дозволу перевантаження (13.3) функція, яка видаляється або недоступна з контексту ініціалізації);
  • якщо Tце тип масиву, кожен елемент ініціалізується за замовчуванням ;
  • в іншому випадку ініціалізація не виконується.

Таким чином

  • new Aлише викликає Aвиклик конструктора s за замовчуванням, який не ініціалізується m. Невизначене значення. Має бути однаковим для new B.
  • new A() інтерпретується відповідно до [dcl.init] / 11 (/ 10 в C ++ 11):

    Об'єкт, ініціалізатором якого є порожній набір дужок, тобто ()повинен бути ініціалізованим значенням.

    А тепер розглянемо [dcl.init] / 8 (/ 7 в C ++ 11 †):

    Для того, щоб значення ініціалізації об'єкта типу Tзасобів:

    • якщо Tє типом класу (можливо, відповідає стандарту cv) (пункт 9), або конструктор за замовчуванням (12.1) або конструктор за замовчуванням, який надається користувачем або видаляється, тоді об’єкт ініціалізується за замовчуванням;
    • якщо T є типом класу (можливо, що відповідає стандарту cv) без наданого користувачем або видаленого конструктора за замовчуванням, тоді об’єкт ініціалізується нулем, а семантичні обмеження для ініціалізації за замовчуванням перевіряються, а якщо T має нетривіальний конструктор за замовчуванням, об’єкт ініціалізований за замовчуванням;
    • якщо Tце тип масиву, то кожен елемент ініціалізується значенням;
    • в іншому випадку об'єкт ініціюється нулем.

    Отже, new A()буде нульова ініціалізація m. І це повинно бути еквівалентно для Aі B.

  • new Cі new C()знову ініціалізує об’єкт, оскільки застосовується перша точка маркера з останньої лапки (C має наданий користувачем конструктор за замовчуванням!). Але, зрозуміло, зараз mініціалізується в конструкторі в обох випадках.


† Ну, цей параграф має дещо інше формулювання в C ++ 11, що не змінює результат:

Для того, щоб значення ініціалізації об'єкта типу Tзасобів:

  • якщо Tє (можливо, cv-кваліфікованим) типом класу (Розділ 9) із наданим користувачем конструктором (12.1), тоді T викликається конструктор за замовчуванням для (і ініціалізація неправильно сформована, якщо T не має доступного конструктора за замовчуванням);
  • якщо Tє (можливо cv-кваліфікованим) типом несоюзного класу без конструктора, що надається користувачем, тоді об'єкт ініціалізується нулем, і якщо Tнеявно оголошений конструктор за замовчуванням нетривіальний, цей конструктор викликається.
  • якщо Tце тип масиву, то кожен елемент ініціалізується значенням;
  • в іншому випадку об'єкт ініціюється нулем.

@ Габріель не дуже.
Коламбо,

а, отже, ви говорите головним чином про с ++ 14, а посилання на с ++ 11 подані в дужках
Габріель

@Gabriel Правильно. Я маю на увазі, що C ++ 14 - це останній стандарт, тож це на перший план.
Коламбо,

1
Прикро в спробі простежити правила ініціалізації за стандартами полягає в тому, що багато змін (найбільше? Всіх?) Між опублікованими стандартами C ++ 14 та C ++ 11 відбувалися через DR, а також де-факто C ++ 11 . А ще є також DR-повідомлення після C ++ 14 ...
TC

@Columbo Я все ще не розумію, чому struct A { int m; }; struct C { C() : m(){}; int m; };дають різні результати і що спричиняє ініціалізацію m у A в першу чергу. Я відкрив спеціальну тему для експерименту, який я зробив, і буду вдячний за ваш внесок у роз’яснення проблеми. Завдяки stackoverflow.com/questions/45290121 / ...
darkThoughts

12

Наступна відповідь розширює відповідь https://stackoverflow.com/a/620402/977038, яка слугувала б посиланням на C ++ 98 та C ++ 03

Цитування відповіді

  1. У C ++ 1998 існує 2 типи ініціалізації: нульова та стандартна
  2. У C ++ 2003 був доданий 3-й тип ініціалізації, ініціалізація значень.

C ++ 11 (з посиланням на n3242)

Ініціалізатори

8.5 ініціалізатор [dcl.init] вказує , що змінна стручок або НЕ стручок може бути инициализирован або в вигляді розпірки або дорівнює-ініціалізатор , які або можуть бути рамно-ініціалізації-лист або ініціалізатор-п сукупно згадується як дужка або-рівнополочні ініціалізатор або використання (список виразів) . До C ++ 11 підтримувався лише (перелік виразів) або пропозиція ініціалізатора, хоча пропозиція ініціалізатора була більш обмеженою, ніж те, що ми маємо в C ++ 11. У C ++ 11 пропозиція ініціалізатора тепер підтримує дужку-init-список, крім вираження призначенняяк це було в C ++ 03. Наступна граматика узагальнює нове підтримуване речення, де жирний шрифт нещодавно додано у стандарт C ++ 11.

ініціалізатор:
    дужка-або-рівний-ініціалізатор
    (список виразів)
дужка-або-рівний-ініціалізатор:
    = стаття ініціалізатора
    braced-init-list
ініціалізатор-стаття:
    присвоєння-вираз
    braced-init-list
ініціалізатор-список:
    ініціалізатор-пункт ... opt
    ініціалізатор-список, ставлення ініціалізатора ... opt **
braced-init-list:
    {список ініціалізаторів, opt}
    {}

Ініціалізація

Як і C ++ 03, C ++ 11 все ще підтримує три форми ініціалізації


Примітка

Частина, виділена жирним шрифтом, була додана в C ++ 11, а викреслена частина була вилучена з C ++ 11.

  1. Тип ініціалізатора: 8.5.5 [dcl.init] _zero-initialize_

Виконується в наступних випадках

  • Об'єкти зі статичною тривалістю або тривалістю зберігання потоків нульово ініціалізовані
  • Якщо ініціалізаторів менше, ніж елементів масиву, кожен елемент, не явно ініціалізований, повинен бути нульовим
  • Під час ініціалізації значення , якщо T є (можливо cv-кваліфікованим) типом класу без об'єднання без конструктора, що надається користувачем, тоді об'єкт ініціюється нулем.

Нульова ініціалізація об’єкта або посилання типу Т означає:

  • якщо T - скалярний тип (3.9), для об'єкта встановлюється значення 0 (нуль), прийняте як інтегральний константний вираз , перетворене в T;
  • якщо T є (можливо, cv-кваліфікованим) типом несоюзного класу, кожен нестатичний член даних і кожен суб'єкт базового класу ініціалізуються нулем, а відступ ініціюється нульовими бітами;
  • якщо T є типом об'єднання (можливо, що відповідає стандарту cv) , перший нестатичний іменований елемент даних об'єкта ініціюється нулем, а відступ ініціюється нульовими бітами;
  • якщо T - тип масиву, кожен елемент нульово ініціалізований;
  • якщо T є еталонним типом, ініціалізація не виконується.

2. Тип ініціалізатора: 8.5.6 [dcl.init] _default-initialize_

Виконується в наступних випадках

  • Якщо новий ініціалізатор опущений, об’єкт ініціалізується за замовчуванням; якщо ініціалізація не виконується, об'єкт має невизначене значення.
  • Якщо для об’єкта не вказано ініціалізатор, об’єкт ініціалізується за замовчуванням, за винятком об’єктів із тривалістю зберігання статичних даних або потоків
  • Коли базовий клас або нестатичний член даних не згадується в списку ініціалізатора конструктора, і цей конструктор викликається.

Ініціалізація за замовчуванням об’єкта типу T означає:

  • якщо T є (можливо cv-кваліфікованим) типом класу не-POD (пункт 9), викликається конструктор за замовчуванням для T (і ініціалізація неправильно сформована, якщо T не має доступного конструктора за замовчуванням);
  • якщо T - тип масиву, кожен елемент ініціалізується за замовчуванням;
  • в іншому випадку ініціалізація не виконується.

Примітка. До С ++ 11 лише ті типи класів, що не є POD, з автоматичною тривалістю зберігання вважалися ініціалізованими за замовчуванням, коли не використовується ініціалізатор.


3. Тип ініціалізатора: 8.5.7 [dcl.init] _value-initialize_

  1. Коли об’єкт (безіменний тимчасовий, іменований змінний, тривалість динамічного зберігання або нестатичний елемент даних), ініціалізатором якого є порожній набір дужок, тобто () або фігурні дужки {}

Ініціалізувати значення об'єкта типу T означає:

  • якщо T є типом класу (можливо, cv-кваліфікованим) (пункт 9) із наданим користувачем конструктором (12.1), тоді викликається конструктор за замовчуванням для T (і ініціалізація неправильно сформована, якщо T не має доступного конструктора за замовчуванням) ;
  • якщо T є (можливо, cv-кваліфікованим) типом несоюзного класу без конструктора, що надається користувачем, тоді кожен нестатичний член даних та компонент базового класу T ініціалізується за значенням; тоді об'єкт ініціюється нулем, і якщо неявно оголошений конструктор за замовчуванням Т нетривіальний, цей конструктор викликається.
  • якщо T - тип масиву, то кожен елемент ініціалізується значенням;
  • в іншому випадку об'єкт ініціюється нулем.

Тож підбиваємо підсумки

Примітка Відповідні цитати зі стандарту виділено жирним шрифтом

  • новий A: ініціалізується за замовчуванням (залишає A :: m неініціалізованим)
  • new A (): Нульова ініціалізація A, оскільки кандидат, ініціалізований значенням, не має наданого користувачем або видаленого конструктора за замовчуванням. якщо T є (можливо cv-кваліфікованим) типом несоюзного класу без конструктора, що надається користувачем, тоді об'єкт ініціюється нулем, і якщо неявно оголошений конструктор за замовчуванням T нетривіальний, цей конструктор викликається.
  • новий B: ініціалізується за замовчуванням (залишає B :: m неініціалізованим)
  • new B (): ініціалізує значення B, що нульово ініціалізує всі поля; якщо T є типом класу (можливо, має кваліфікацію cv) (Розділ 9) із наданим користувачем конструктором (12.1), то конструктор за замовчуванням для T називається
  • новий C: за замовчуванням ініціалізує C, який викликає ctor за замовчуванням. якщо T є типом класу (можливо, відповідає стандарту cv) (Розділ 9), викликається конструктор за замовчуванням для T. Більше того, якщо новий ініціалізатор опущений, об'єкт ініціалізований за замовчуванням
  • new C (): значення ініціалізує C, який викликає типовий ctor. якщо T є типом класу (можливо, має кваліфікацію cv) (Розділ 9) із наданим користувачем конструктором (12.1), тоді для T викликається конструктор за замовчуванням. Більше того, об’єкт, ініціалізатором якого є порожній набір дужок, тобто (), повинен бути ініціалізованим значенням

0

Я можу підтвердити, що в C ++ 11 все, що згадується у питанні під C ++ 14, є правильним, принаймні відповідно до реалізацій компілятора.

Щоб перевірити це, я додав наступний код до свого тестового набору . Я тестував -std=c++11 -O3у GCC 7.4.0, GCC 5.4.0, Clang 10.0.1 та VS 2017, і всі тести нижче пройшли.

#include <gtest/gtest.h>
#include <memory>

struct A { int m;                    };
struct B { int m;            ~B(){}; };
struct C { int m; C():m(){}; ~C(){}; };
struct D { int m; D(){};             };
struct E { int m; E() = default;     };
struct F { int m; F();               }; F::F() = default;

// We use this macro to fill stack memory with something else than 0.
// Subsequent calls to EXPECT_NE(a.m, 0) are undefined behavior in theory, but
// pass in practice, and help illustrate that `a.m` is indeed not initialized
// to zero. Note that we initially tried the more aggressive test
// EXPECT_EQ(a.m, 42), but it didn't pass on all compilers (a.m wasn't equal to
// 42, but was still equal to some garbage value, not zero).
//
#define FILL { int m = 42; EXPECT_EQ(m, 42); }

// We use this macro to fill heap memory with something else than 0, before
// doing a placement new at that same exact location. Subsequent calls to
// EXPECT_EQ(a->m, 42) are undefined behavior in theory, but pass in practice,
// and help illustrate that `a->m` is indeed not initialized to zero.
//
#define FILLH(b) std::unique_ptr<int> bp(new int(42)); int* b = bp.get(); EXPECT_EQ(*b, 42)

TEST(TestZero, StackDefaultInitialization)
{
    { FILL; A a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; B a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; C a; EXPECT_EQ(a.m, 0); }
    { FILL; D a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; F a; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackValueInitialization)
{
    { FILL; A a = A(); EXPECT_EQ(a.m, 0); }
    { FILL; B a = B(); EXPECT_EQ(a.m, 0); }
    { FILL; C a = C(); EXPECT_EQ(a.m, 0); }
    { FILL; D a = D(); EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a = E(); EXPECT_EQ(a.m, 0); }
    { FILL; F a = F(); EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackListInitialization)
{
    { FILL; A a{}; EXPECT_EQ(a.m, 0); }
    { FILL; B a{}; EXPECT_EQ(a.m, 0); }
    { FILL; C a{}; EXPECT_EQ(a.m, 0); }
    { FILL; D a{}; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a{}; EXPECT_EQ(a.m, 0); }
    { FILL; F a{}; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, HeapDefaultInitialization)
{
    { FILLH(b); A* a = new (b) A; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); B* a = new (b) B; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); C* a = new (b) C; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); F* a = new (b) F; EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapValueInitialization)
{
    { FILLH(b); A* a = new (b) A(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D(); EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F(); EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapListInitialization)
{
    { FILLH(b); A* a = new (b) A{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D{}; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F{}; EXPECT_EQ(a->m, 42); } // ~UB
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Місцями, де UB!згадується, є невизначена поведінка, і фактична поведінка, ймовірно, залежатиме від багатьох факторів ( a.mможе дорівнювати 42, 0 або іншому сміттю). Місця, де ~UBце згадується, також є теоретично невизначеною поведінкою, але на практиці через використання нового розміщення дуже малоймовірно, ніж a->mбуде дорівнювати чомусь іншому, ніж 42.

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