Що таке Ініціалізатор у С




Що таке Ініціалізатор у С



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

Ініціалізатор визначає початкове значення змінної. Можна ініціалізувати змінні у цих контекстах:

    У визначенні змінної:

Point get_new_point(int x, int y) < return < x, y >; > Point get_new_point(int x, int y) < return Point< x, y >; >

Ініціалізатори можуть набувати цих форм:

    Вираз (або розділений комами список виразів) у дужках:

struct Point< int x; int y; >; class PointConsumer< public: void set_point(Point p)<>; void set_points(initializer_list my_list)<>; >; int main() < PointConsumer pc<>; pc.set_point(<>); pc.set_point(< 3, 4 >); pc.set_points(< < 3, 4 >, < 5, 6 >>); >

Типи ініціалізації

Існує кілька типів ініціалізації, які можуть траплятися на різних етапах виконання програми. Різні види ініціалізації не є взаємовиключними, наприклад, ініціалізація списку може активувати ініціалізацію значень та інших обставин, що може активувати статистичну ініціалізацію.

Нульова ініціалізація

Нульова ініціалізація — завдання змінної нульового значення, неявно перетвореного на тип:

  • Числові змінні ініціалізуються значенням 0 (або 0,0; 0,0000000000 і т.п.).
  • Змінні char ініціалізовані в '\0'.
  • Вказівники ініціалізовані в nullptr .
  • Масиви, класи POD, структури та об'єднання ініціалізують своїх членів до нуля.

Нульова ініціалізація виконується у різний час:

  • При запуску програми - всім іменованих змінних, мають статичну тривалість. Далі ці змінні можуть бути повторно ініціалізовані.
  • Під час ініціалізації значень – для скалярних типів та типів класу POD, які ініціалізуються за допомогою порожніх фігурних дужок.
  • Для масивів, у яких ініціалізовано лише підмножина членів.

Нижче наведено деякі приклади нульової ініціалізації:

struct my_struct< int i; char c; >; int i0; // zero-initialized to 0 int main() < static float f1; // zero-initialized to 0.000000000 double d<>; // zero-initialized to 0.00000000000000000 int * ptr <>; // initialized to nullptr char s_array[3]; // the third char is initialized to '\0' int int_array[5] = < 8, 9, 10 >; // the fourth and fifth ints are initialized to 0 my_struct a_struct<>; // i = 0, c = '\0' >

Ініціалізація за умовчанням

Ініціалізація за умовчанням для класів, структур та об'єднань – це ініціалізація за допомогою конструктора за умовчанням. Конструктор за замовчуванням можна викликати без вираження ініціалізації чи ключового new слова:

MyClass mc1; MyClass* mc3 = New MyClass;

Якщо клас, структура або об'єднання не мають конструктора за замовчуванням, компілятор видає помилку.

Скалярні змінні за замовчуванням ініціалізовані, якщо вони визначені без ініціалізації. Вони мають невизначені значення.

int i1; float f; char c;

За замовчуванням масиви ініціалізовані при визначенні без вираження ініціалізації. Якщо масив ініціалізується за умовчанням, його члени ініціалізуються за умовчанням і набувають невизначених значень як у наведеному нижче прикладі.

Якщо елементи масиву не мають стандартного конструктора, компілятор видає помилку.

Ініціалізація за умовчанням константних змінних

Константні змінні необхідно оголошувати разом із ініціалізатором. Якщо вони скалярні типи, вони викликають помилку компілятора і якщо вони типи класів з конструктором за замовчуванням, вони попереджають:

class MyClass<>; int main() < // const int i2; // compiler error C2734: const object must be initialized if not extern //const char c2; // same error const MyClass mc1; // compiler error C4269: 'const automatic data initialized with compiler generated default constructor produces unreliable results >

Ініціалізація за умовчанням статичних змінних

Статичні змінні, оголошені без ініціалізації, ініціалізуються значенням 0 (з неявним перетворенням до відповідного типу).

class MyClass < private: int m_int; char m_char; >; int main() < static int int1; // 0 static char char1; // '\0' static bool bool1; // false static MyClass mc1; //>

Додаткові відомості про ініціалізацію глобальних статичних об'єктів див. у розділі "Основні аргументи функції та командного рядка".

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

Ініціалізація значення відбувається у таких випадках:

  • Іменоване значення ініціалізується з використанням порожніх фігурних дужок.
  • Анонімний часовий об'єкт ініціалізується за допомогою порожніх круглих або фігурних дужок.
  • Об'єкт ініціалізується ключовим new словом та порожніми дужками або фігурними дужками

При ініціалізації значення виконуються такі дії:

  • Для класів, які мають хоча б один відкритий конструктор, викликається за замовчуванням конструктор.
  • Для класів nonunion без оголошених конструкторів об'єкт ініціалізований з нуля і викликається за замовчуванням конструктор.
  • Що стосується масивів кожен елемент ініціалізується значенням.
  • В інших випадках змінна ініціалізується нульовим значенням.
class BaseClass < private: int m_int; >; int main() < BaseClass bc<>; // class is initialized BaseClass* bc2 = new BaseClass(); // class is initialized, m_int value is 0 int int_arr[3]<>; // value of all members is 0 int a<>; // value of a is 0 double b<>; // value of b is 0.00000000000000000 >

Ініціалізація копіюванням

Ініціалізація копіюванням – це ініціалізація одного об'єкта з використанням іншого об'єкта. Вона виконується у таких випадках:

  • Змінна ініціалізується за допомогою знака рівності.
  • Аргумент передається на функцію.
  • Об'єкт повертається функцією.
  • Виникає чи перехоплюється виняток.
  • Нестатичний елемент даних ініціалізується за допомогою знаку рівності.
  • Клас, структура та члени об'єднання ініціалізуються із застосуванням ініціалізації шляхом копіювання під час агрегатної ініціалізації. Приклади ініціалізації агрегату див. у розділі "Агрегатна ініціалізація".

Наступний код показує кілька прикладів ініціалізації копіюванням.

#include using namespace std; class MyClass < public: MyClass(int myInt) <>void set_int(int myInt) < m_int = myInt; >int get_int() const < return m_int; >private: int m_int = 7; // copy initialization of m_int>; class MyException : public exception<>; int main() < int i = 5; // copy initialization of i MyClass mc1<i>; MyClass mc2 = mc1; // copy initialization of mc2 from mc1 MyClass mc1.set_int(i); // copy initialization of parameter from i int i2 = mc2.get_int(); // copy initialization of i2 from return value of get_int() try < throw MyException(); >catch (MyException ex) < // copy initialization of ex cout >

Ініціалізація копіювання неспроможна викликати явні конструктори.

vector v = 10; // the constructor is explicit; compiler error C2440: can't convert from 'int' to 'std::vector>' regex r = "a.*b"; // the constructor is explicit; same error shared_ptr sp = новий int (1729); // the constructor is explicit; same error

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

Пряма ініціалізація

Пряма ініціалізація - це ініціалізація з використанням (непорожніх) круглих або фігурних дужок. На відміну від копіювання ініціалізації вона може викликати явні конструктори. Вона виконується у таких випадках:

  • Змінна ініціалізується за допомогою пустих круглих або фігурних дужок.
  • Змінна ініціалізована за допомогою ключового new слова плюс непусті дужки чи дужки
  • Змінна ініціалізується за допомогою static_cast
  • У конструкторі базові класи та нестатичні члени ініціалізуються за допомогою списку ініціалізації.
  • У копії захопленої змінної у лямбда-вираженні.

Нижче наведений код демонструє кілька прикладів прямої ініціалізації.

class BaseClass < public: BaseClass(int n) :m_int(n)<>// m_int is direct initialized private: int m_int; >; class DerivedClass : Public BaseClass < public // BaseClass and m_char є direct initialized DerivedClass(int n, char c) : BaseClass(n), m_char(c) <>private: char m_char; >; int main()< BaseClass bc1(5); DerivedClass dc1< 1, 'c' >; BaseClass * bc2 = New BaseClass (7); BaseClass bc3 = static_cast(dc1); int a = 1; function func = [a]()< return a + 1; >; // a is direct initialized int n = func(); >

Ініціалізація списком

Ініціалізація списком виконується, коли змінна ініціалізується за допомогою списку ініціалізації у фігурних дужках.Списки ініціалізації у фігурних дужках можна використовувати у таких випадках:

  • Ініціалізується змінна.
  • Клас ініціалізується ключовим словом new
  • Об'єкт повертається функцією.
  • Аргумент передається функції.
  • Один із аргументів при прямій ініціалізації.
  • В ініціалізаторі нестатичних елементів даних.
  • У списку ініціалізації конструктора.

Нижче наведений код демонструє кілька прикладів ініціалізації списком.

class MyClass < public: MyClass(int myInt, char myChar) <>private: int m_int[]< 3 >; char m_char; >; class MyClassConsumer < public: void set_class(MyClass c) <>MyClass get_class() < return MyClass< 0, '\0' >; > >; struct MyStruct< int my_int; char my_char; MyClass my_class; >; int main() < MyClass mc1< 1, 'a' >; MyClass* mc2 = new MyClass< 2, 'b' >; MyClass mc3 = <3, 'c'>; MyClassConsumer mcc; mcc.set_class(MyClass< 3, 'c' >); mcc.set_class(< 4, 'd' >); MyStruct ms1 < 1, 'a', < 2, 'b' >>; >

Агрегатна ініціалізація

Агрегатна ініціалізація - форма ініціалізації списку для масивів та типів класів (часто структур та об'єднань), з наступними характеристиками:

  • Відсутність закритих чи захищених членів.
  • Відсутність заданих користувачем конструкторів, крім явно заданих за замовчуванням або віддалених конструкторів.
  • Відсутність базових класів.
  • Відсутність віртуальних функцій-членів.

У Visual Studio 2015 і раніше версіях агрегат не може мати ініціалізатори фігурних дужок або рівних значень для нестатичних елементів. Це обмеження було видалено у стандарті C++14 та реалізовано у Visual Studio 2017.

Агрегатні ініціалізатори складаються зі списку ініціалізації у фігурних дужках зі знаком рівності або без нього як у наведеному нижче прикладі:

#include using namespace std; struct MyAggregate< int myInt; char myChar; >; struct MyAggregate2< int myInt; char myChar = 'Z'; // member-initializer OK in C++14 >; int main() < MyAggregate agg1< 1, 'c' >; MyAggregate2 agg2; cout; int myArr2[3] = <5, 6, 7>; int myArr3[5] = < 8, 9, 10>; cout cout cout

Повинен з'явитися наступний результат:

agg1: c: 1 agg2: Z: 2 myArr1: 1 2 3 4 myArr3: 8 9 10 0 0

Елементи масиву, оголошені, але явно ініціалізовані під час статистичної ініціалізації, як показано вище myArr3 .

Ініціалізація об'єднань та структур

Якщо об'єднання немає конструктора, його можна ініціалізувати з одним значенням (або з іншим екземпляром об'єднання). Значення використовується ініціалізації першого нестатичного поля. Це відрізняється від ініціалізації структур, де перше значення в ініціалізаторі використовується для ініціалізації першого поля, друге — для ініціалізації другого поля і т.д.

struct MyStruct < int myInt; char myChar; >; union MyUnion < int my_int; char my_char; bool my_bool; MyStruct my_struct; >; int main() < MyUnion mu1< 'a' >; // my_int = 97, my_char = 'a', my_bool = true, MyUnion mu2 <1>; // my_int = 1, my_char = 'x1', my_bool = true, MyUnion mu3 <>; // my_int = 0, my_char = '\ 0', my_bool = false, MyUnion mu4 = mu3; // my_int = 0, my_char = '\0', my_bool = false, // MyUnion mu5 < 1, 'a', true >; // compiler error: C2078: too many initializers //MyUnion mu6 = 'a'; // compiler error: C2440: cannot convert from 'char' to 'MyUnion' //MyUnion mu7 = 1; // compiler error: C2440: cannot convert from 'int' to 'MyUnion' MyStruct ms1< 'a' >; // myInt = 97, myChar = '\0' MyStruct ms2<1>; // myInt = 1, myChar = '\0' MyStruct ms3<>; // myInt = 0, myChar = '\ 0' MyStruct ms4; // myInt = 1, myChar = 'a' MyStruct ms5 = < 2, 'b'>; // myInt = 2, myChar = 'b'>

Ініціалізація статистичних виразів, що містять статистичні вирази

Агрегатні типи можуть містити інші агрегатні типи, наприклад масиви масивів, масиви структур тощо. Ці типи ініціалізуються за допомогою вкладених наборів фігурних дужок, як показано в наступному прикладі:

struct MyStruct < int myInt; char myChar; >; int main() < int intArr1[2][2], < 3, 4 >>; int intArr3[2][2] = ; MyStruct structArr[]< < 1, 'a' >, < 2, 'b' >, >; >

Ініціалізація посилань

Змінні посилання типу повинні ініціалізуватися об'єктом типу, на якому заснований тип посилання, або об'єктом типу, який можна перетворити на такий тип. Наприклад:

// initializing_references.cpp int iVar; long lVar; int main() < long& LongRef1 = lVar; // No conversion required.long& LongRef2 = iVar; // Error C2440 const long & LongRef3 = iVar; // OK LongRef1 = 23L; // Change lVar through a reference. LongRef2 = 11L; // Change iVar through a reference. LongRef3 = 11L; // Error C3892 >

Єдиний спосіб ініціалізувати посилання за допомогою тимчасового об'єкта ініціалізація постійного тимчасового об'єкта. Після ініціалізації змінна типу посилань завжди вказує на той же об'єкт; Його не можна змінити, щоб вказати на інший об'єкт.

Хоча синтаксис може бути однаковим, ініціалізація змінних посилального типу та присвоєння значень змінним посилання типу семантично різняться. У попередньому прикладі присвоювання, які змінюють значення змінних iVar та lVar, виглядають аналогічно ініціалізації, але мають інший ефект. Ініціалізація визначає об'єкт, який вказує змінна посилального типу; при присвоюванні через посилання проводиться присвоєння значення об'єкту, який вказує посилання.

Оскільки передача аргументу типу посилання в функцію і повернення значення типу посилання з функції є ініціалізацією, формальні аргументи функції, а також посилання, що повертаються, ініціалізуються правильно.

Змінні посилання типу можна оголошувати без ініціалізаторів тільки в наведених нижче випадках.

    Оголошення функцій (прототипи). Наприклад:

При ініціалізації змінної типу посилання компілятор використовує граф прийняття рішень, показаний на наступному малюнку, для вибору між створенням посилання на об'єкт або створенням тимчасового об'єкта, до якого вказуються еталонні точки:

Графік прийняття рішень починається з: чи є ініціалізатор lvalue того ж типу або типом, похідним від типу посилання? Якщо так, посилання посилається на об'єкт, вказаний в ініціалізаторі. Якщо ні, наступне рішення полягає в тому, чи є змінна посилання типу ініціалізованим посиланням const T, і чи може ініціалізатор бути неявно перетворений в T? Якщо так, створюється тимчасова змінна, а еталонна змінна стає іменем цього тимчасового. Якщо ні, то це помилка.

Граф прийняття рішень для ініціалізації типів посилань

Посилання на volatile типи (оголошені як volatile ідентифікатор typename & ) можна ініціалізувати з volatile об'єктами того ж типу або з об'єктами, які не були оголошені як . volatile Однак вони не можуть бути ініціалізовані з const об'єктами цього. Аналогічно посилання на const типи (оголошені як const ідентифікатор typename & ) можна ініціалізувати з const об'єкти того ж типу (або все, що має перетворення в цей тип або об'єкти, які не були оголошені як ). const Однак вони не можуть бути ініціалізовані з volatile об'єктами цього.

Посилання, які не є кваліфікованими за допомогою ключового слова або volatile ключового const слова, можна ініціалізувати тільки з об'єктами, оголошеними як жоден з const volatile них.

Ініціалізація зовнішніх змінних

Оголошення автоматичних, статичних та зовнішніх змінних можуть містити ініціалізатори. Однак, оголошення зовнішніх змінних можуть містити ініціалізатори, тільки якщо змінні не оголошені як extern .

Що означає ініціалізувати int C++?

Нещодавно я отримав поштою від Сема Джонсона це питання.Ось трохи відредагований лист Сема:

«Візьмемо для прикладу цей код у локальній області видимості функції:

Багато людей вважають, що ініціалізація відбувається у рядку 1, тому що веб-сайти на кшталт cppreference дають таке визначення: "Ініціалізація змінної надає його початкове значення на момент створення".

Однак я переконаний, що ініціалізація відбувається у рядку 2, тому що [у різних хороших книгах по C++] ініціалізація визначається як перше суттєве значення, що потрапляє до змінної.

Чи можете ви сказати, який рядок вважається ініціалізацією?

Чудове питання. На Cppreference написано правильно, і всім класових типів відповідь проста: об'єкт ініціалізується у рядку 1 викликом його стандартного конструктора.

Але (а ви знали, що буде «але») для локального об'єкта фундаментального вбудованого типу на зразок int відповідь буде. трохи складнішим. І саме тому Сем поставив це питання, адже він знає, що мова досить вільно поводиться з ініціалізацією таких локальних об'єктів з історичних причин, які на той час мали сенс.

Коротка відповідь: цілком припустимо говорити, що змінна набуває свого вихідного значення у рядку 2. Але зауважте, що я навмисно не сказав «Об'єкт ініціалізується в рядку 2», до того ж і код, і ця відповідь обходить мовчанням важливіше питання: «Ну добре, а що, якщо код між рядками 1 та 2 спробує вважати значення об'єкта?»

Ця посада складається з трьох частин:

  • До C++26 ситуація була досить незручною. Але найцікавіше те, як це описується сьогодні в Стандарті, нижче я не втримався від цитування.
  • У C++26 ми зробили цей код безпечним за умовчанням, Дякувати за це варто Томас Кеппе! То був дуже важливий крок.
  • У моєму експерименті Cpp2 ця проблема повністю зникла і всі типи обробляються однаково, з гарантованою безпекою ініціалізації. Я хочу запропонувати таке рішення для самого ISO C++ після C++26, щоб ISO C++ міг еволюціонувати і повністю позбавитися цієї проблеми в майбутньому, якщо складеться консенсус про внесення такої зміни.

Давайте почнемо із сучасності, зі статус-кво, що склався до випуску C++26…

Відповідь до C++26: змінна ніколи не «ініціалізується»

У випадку кількох вбудованих типів, наприклад, int відповідь полягає в тому. що в даному прикладі взагалі не відбувається ініціалізації, тому що (строго кажучи) жодна з рядків не виконує ініціалізації. Якщо вас це дивує, то пояснення:

  • У рядку 1 оголошується неініціалізований об'єкт. Він не має початкового значення, ні явного, ні непрямого.
  • Далі у рядку 2 присвоюється "Початкове значення". Ця операція перезаписує біти об'єкта і надає йому те значення, що й біти, ініціалізовані в рядку 1… але це присвоєння, а чи не ініціалізація (конструкція).

Тим не менш, я думаю, розумно буде неформально назвати рядок 2 «завданням початкового значення» у тому сенсі, що це записування в цей об'єкт першого суттєвого для програми значення. З формальної точки зору це не ініціалізація, але зрештою біти стають однаковими, і в хороших книгах рядок 2 можуть резонно називати «ініціалізацією a».

«Але заждіть», — може сказати хтось. «Вчора ввечері я читав Стандарт, і в [dcl.init] говориться, що рядок 1 це і є "Ініціалізація значенням за замовчуванням"! Тобто рядок 1 та є ініціалізація!» На ці твердження я можу відповісти так і ні.Давайте ж поглянемо на формальний точний і досить кумедний відповідь з Стандарту, він просто чудовий: Стандарт дійсно говорить, що в рядку 1 об'єкт ініціалізується значенням за умовчанням… але, для типів на кшталт int термін "ініціалізується значенням за умовчанням" позначає "ініціалізація не виконується".

(Саме час сказати: «Стандарт - це не туторіал». Іншими словами, не варто читати Стандарт для вивчення мови. Стандарт досить чітко описує дії C++, і немає нічого поганого в тому, що він визначає все таким чином, це абсолютно нормально. Але він не написаний для обивателя, і ніхто не звинуватить вас, якщо ви подумаєте, що «ініціалізація значенням за умовчанням означає відсутність ініціалізації» — це приклад когнітивного дисонансу, оруелівського дводумства (це не те саме) або пасивно-агресивної провокації.)

Чи можна поставити близьке цьому питання: чи почався час життя об'єкта після рядка 1? Хороші новини полягають у тому, що так, у рядку 1 справді почався час життя неініціалізованого об'єкта, згідно з параграфом 1 [basic.life]. Але давайте не будемо надто вдаватися до розбору фрази про «порожню ініціалізацію» з цього параграфа, бо це ще одна алегорія Стандарту тієї ж концепції «це ініціалізація, хоча ні, ми просто пожартували». (Я вже казав, що Стандарт — це не туторіал?) І, зрозуміло, це серйозна проблема, адже час життя об'єкта вже почався, але він ще не ініціалізований передбачуваним значенням. Це найгірша проблема неініціалізованої змінної, адже зчитування з неї може становити загрозу безпеці; це справжня «невизначена поведінка», здатна на що завгодно, і нападники можуть використовувати цю властивість.

На щастя, в C++26 ситуація з безпекою стає набагато краще.

C++26: все стає кращим (насправді) і безпечним за умовчанням

Лише кілька місяців тому (у березні 2024 року, на нараді в Токіо) ми покращили цю ситуацію в C++26, запровадивши статтю Томаса Кеппе P2795R5, «Erroneous behavior for uninitialized reads». Можливо, її назва може здатися знайомою для читачів мого блогу, адже я згадував її у своїй звіті про подорож до Токіо.

У C++26 було створено нову концепцію помилкової поведінки (erroneous behavior)яка краще «невизначеного» або «неуточненого», адже вона дозволяє нам міркувати про код «який точно визначений як помилковий»(Серйозно, це майже пряма цитата зі статті), а оскільки код тепер точно визначений, ми позбавляємося загрози безпеці, пов'язаної з «невизначеною поведінкою». Можна сприймати це як інструмент Стандарту, що дозволяє перетворити якусь поведінку з «лякаюче невизначеного» на «що ж, частково це наша вина, тому що ми дозволили вам написати цей код, який означає не те, що має означати, але насправді ви написали тут баг, і ми поставимо огорожу навколо цієї ями з кілками, щоб за умовчанням ви не падали в неї». І вперше ця концепція була застосована до. барабанний дріб. неініціалізованим локальним змінним.

І це дуже важливо, тому що означає, що рядок 1 з вихідного прикладу, як і раніше, не ініціалізований, але починаючи з C++26 це стає «помилковою поведінкою», тобто при складанні коду компілятором C++26 невизначена поведінка не може виникнути під час читання неініціалізованого значення. Так, з цього випливає, що компілятор C++26 генеруватиме код, що відрізняється від попереднього.Він гарантовано запише відоме компілятору помилкове значення (але це не гарантує, що на нього може покластися програміст, так що до нього, як і раніше, нуль довіри), якщо є хоч якась ймовірність, що значення можуть вважати.

Це здається несуттєвим, але насправді це важливе покращення, що доводить, що комітет серйозно налаштований на активну зміну нашої мови у бік її безпеки за умовчанням. Тенденцію збільшення обсягів безпечного за умовчанням коду ми спостерігатимемо у найближчому майбутньому C++, і це можна лише вітати.

Поки ви очікуєте, що ваш улюблений компілятор C++26 додасть підтримку цього, можна отримати апроксимацію цієї функції за допомогою перемикача GCC або Clang -ftrivial-auto-var-init=pattern або за допомогою перемикача MSVC /RTC1 (поспішайте використовувати їх, якщо можете). Вони дадуть вам практично все те, що дасть C++26, за винятком, можливо, того, що не створюватимуть діагностики (наприклад, перемикач Clang створює діагностику, тільки якщо запустити Memory Sanitizer).

Наприклад, розглянемо, як ця нова поведінка за замовчуванням перешкоджає витіканню секретів, на прикладі програми, скомпільованої з сьогоднішнім прапором і без нього (посилання на Godbolt):

template auto print(char (&a)[N]) < std::cout auto f1() < char a[] = ; print(a); > auto f2() < char a[6]; print(a); // сьогодні цей код, мабуть, виведе "secret" >auto f3() < char a[] = ; print(a); // перезаписує "secret" > int main()

Стандартно всі три локальних масиву використовують те саме стекове сховище, і після того, як f1 поверне рядок secret , вона, ймовірно, все ще перебуватиме в стеку, очікуючи, що на неї накладеться масив f2 .

У сьогоднішньому C++ за замовчуванням без -ftrivial-auto-var-init=pattern або /RTC1 функція f2, ймовірно, виведе secret. Що може викликати, скажімо так, проблеми безпеки та захисту. Така невизначена поведінка правила відсутності ініціалізації створює погану репутацію C++.

Але при використанні -ftrivial-auto-var-init=pattern компіляторів GCC та Clang або /RTC1 компілятора MSVC , а також починаючи з C++26 і далі за умовчанням функція f2 не призведе до витоку секрету. Як іноді говорить Бйорн в інших контекстах, це прогрес! А тим буркунам, хто, можливо, хотів би сказати: «Автор, я звик до небезпечного коду, рятування від небезпечного коду за умовчанням суперечить духу C++», відповім, що (а) таке справжнє і (б) звикайте до цього, тому що подібного надалі буде набагато більше.

Доповнення: часто ставлять питання про те, чому б не ініціалізувати змінну значенням 0? Це пропонують постійно, але це не найкраща відповідь з багатьох причин. Ось дві основні: (1) нуль який завжди буває істотним для програми значенням, отже ін'єктування його часто призводить до заміни одного бага іншим; (2) часто він активно маскує від санітайзерів збої ініціалізації, тому ми не можемо побачити помилку та повідомити про неї. Використання певного реалізацією добре відомого «помилкового» бітового патерну не призводить до таких проблем.

Але це C++, так що ви завжди можете при необхідності взяти повний контроль у свої руки і отримати максимальну продуктивність.Так що так, при сильному бажанні C++26 дозволяє відмовитися від цього, написавши [[indeterminate]] , але кожне використання цього атрибута має піддаватися перевірці при кожному реву коду і мати чітке виправдання у вигляді точних вимірювань продуктивності, що демонструють необхідність перевизначення безпечної поведінки за замовчуванням:

int a [[indeterminate]] ; // Так у C++26 можна сказати "так, будь ласка, зроби мені боляче, // мені потрібна ця стара небезпечна семантика"

Після C++26: що ми можемо зробити?

Ось яка у нас ситуація до C++26 (найпроблемніші рядки — 4 і 5):

// У сучасному C++ до C++26 для локальних змінних // Застосування фундаментального типу на кшталт 'int' int a; // оголошення без ініціалізації std::cout 

Варто відзначити, що рядок 5 може і нічого не виводити ... це невизначена поведінка, так що вам пощастить, якщо питання буде тільки у висновку і не виведенні, адже компілятор, що відповідає стандартам, теоретично може згенерувати код, що стирає жорсткий диск, що викликає nasal demons або приводить до іншим традиційним проказам невизначеної поведінки.

А ось з чого ми починаємо в C++26 (відмінності знаходяться у рядках 4 та 5):

// C++26 для локальних змінних // Застосування фундаментального типу на кшталт 'int' int a; // Декларація з певним помилковим значенням std::cout 

Хороші новини: тепер наші жорсткі диски безпеки: реалізація може вивести значення або перервати виконання, але невизначеної поведінки не буде.

Дрібним шрифтом: компілятори C++26 повинні змусити рядок 4 переписати біти відомим значенням, і вмотивовані повідомити проблему рядку 5 (але не повинні цього).

У моєму експериментальному синтаксисі Cpp2 локальні змінні всіх типів визначаються так: a: some_type = initial_value; . Можна опустити частину з = initial_value , щоб дати зрозуміти, що простір стека виділено під змінну, але її ініціалізація відкладена, після чого Cpp2 гарантує ініціалізацію до використання; ви зобов'язані виконати ініціалізацію пізніше за допомогою = (наприклад, a = initial_value; ), перш ніж якось використовувати змінну, що забезпечує нам гнучкість, наприклад, дозволяє використовувати різні конструктори для однієї і тієї ж змінної різними шляхами розгалуження. Тобто еквівалентний приклад буде таким (відмінності від C++26 знаходяться у рядках 4-6 та 10-12):

// Локальні змінні в моєму синтаксисі Cpp2 // Застосування фундаментального типу на кшталт 'int' a: int; // виділяє простір без ініціалізації // std::cout реальна ініціалізація! std::cout реальна ініціалізація! std::cout 

У Cpp2 навмисно не залишено простих способів відмовитися від такої схеми та використовувати змінну до її ініціалізації. Щоб досягти цього, потрібно створити в стеку масив сирих std::byte або щось подібне, а потім виконати unsafe_cast, щоб прикинутися, що це інший тип. Писати це довго і складно, адже я вважаю, що небезпечний код має бути довгим та складним у написанні… але його можна при необхідності написати, тому що така природа C++: я можу засуджувати небезпечний код, який ви захочете написати заради продуктивності, але я до смерті захищатиму ваше право писати його за необхідності; C++ завжди дозволяє залізти всередину та взяти управління на себе.Я прагну перейти від моделі «продуктивність за умовчанням, безпека завжди доступна», в якій для забезпечення безпеки потрібно докладати зусиль, до моделі «безпека за умовчанням, продуктивність завжди доступна». Я вигадав для цього таку метафору: мені не хочеться відбирати у програмістів на C++ гострі ножі, тому що шеф-кухарям іноді потрібні гострі ножі; але коли ножами не користуються, ми просто хочемо покласти їх у ящик, який потрібно усвідомлено відчиняти, а не розкидати їх по підлозі та постійно нагадувати людям, щоб вони дивилися під ноги.

Поки ця модель працює дуже добре і має потрійну перевагу: продуктивність (ініціалізація не виконується, поки вам це не потрібно), гнучкість (можна викликати той реальний конструктор, який мені потрібен), безпека (реальна «ініціалізація» з реальною конструкцією і ніколи не виникає ситуації використання до ініціалізації). Думаю, колись це може з'явитися і в ISO C++, і я маю намір через рік-два відправити пропозицію в цьому стилі комітету ISO C++, зробивши її максимально переконливою. Можливо, комітету воно сподобається, або він знайде непомічені мною недоліки. Подивимося! Як би там не було, я повідомлятиму про новини у своєму блозі.

Ще раз дякую Сему Джонсону за це питання!

Схожі статті

  • Що таке Ципру
  • Що таке режим Atti для дронів Drones Cameras
  • Що таке Адамове яблуко у чоловіків
  • Що таке добори на міжкімнатні
  • Що таке дека на газонокосарці
  • Що таке код помилки 601
  • Що таке Жилка яловича
  • Що таке фонетична норма
  • Недавні статті

  • Як бродить зернова брага
  • Що робити якщо не засмагаєш на сонці чому засмага погано лягає на шкіру або перестає прилипати
  • Як швидко зняти гель лак без апарату
  • Як робиться Каті голови
  • Яка гребінець краще для об'єму
  • Чим роблять м'яку покрівлю
  • Чи можна залишати крем для обличчя на ніч
  • Де знаходиться датчик селектора
  • географія нашої діяльності
    вулиця Драгоманова, 27
    вул. Курчатова 1Б
    вул. Міцкевича 130
    вул. Лабунського, 1
    вул. Макарова-Пржевальського
    вул. Толстого 10
    вул. Грушевського 28
    вул. Перший промінь (Черняхівського)
    напишіть нам

    сообщение успешно отправлено
    x