Короткий запитальник по C + +. Частина 3 (FAQ)

[10.11] Що таке помилка в порядку статичної ініціалізації ("static initialization order fiasco")?


Непомітний і підступний спосіб вбити ваш проект.


Помилка порядку статичної ініціалізації – це дуже тонкий і часто невірно сприймається аспект С + +. На жаль, подібну помилку дуже складно відловити, оскільки вона відбувається до входження у функцію main() .


Уявіть собі, що у вас є два статичних об'єкта x і y , Які перебувають у двох різних вихідних файлах, скажімо x.cpp і y.cpp. І шлях конструктор об'єкту y викликає якийсь метод об'єкта x .


Ось і все. Так просто.


Проблема в тому, що у вас рівно п'ятдесятивідсоткову можливість катастрофи. Якщо трапиться, що одиниця трансляції з x.cpp буде проініціалізувати першої, то все в порядку. Якщо ж першою буде проініціалізувати одиниця трансляції файлу y.cpp, тоді конструктор об'єкту y буде запущено до конструктора x , І вам кришка. Тобто, конструктор y викличе метод об'єкту x , Коли сам x ще не створений.


Ідіть працювати в МакДональдс. Робіть Біг-Маки, забудьте про класи.


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


Примітка: помилки статичної ініціалізації не поширюються на базові / вбудовані типи, такі як int або char* . Наприклад, якщо ви створюєте статичну змінну типу float , У вас не буде проблем з порядком ініціалізації. Проблема виникає лише тоді, коли у вашого статичного чи глобального об'єкта є конструктор.


[10.12] Як запобігти помилку в порядку статичної ініціалізації?


Використовуйте "створення при першому використанні", тобто, помістіть ваш статичний об'єкт у функцію.


Уявіть собі, що у нас є два класи Fred і Barney . Є глобальний об'єкт типу Fred , З ім'ям x , І глобальний об'єкт типу Barney , з ім'ям y . Конструктор Barney викликає метод goBowling() об'єкта x . Файл x.cpp містить визначення об'єкта x :

    // File x.cpp
#include “Fred.hpp”
Fred x;

Файл y.cpp містить визначення об'єкта y :

    // File y.cpp
#include “Barney.hpp”
Barney y;

Для повноти уявімо, що конструктор Barney::Barney() виглядає наступним чином:

    // File Barney.cpp
#include “Barney.hpp”

Barney::Barney()
{
// …
x.goBowling();
// …
}


Як описано вище [10.11], проблема трапляється, якщо y створюється раніше, ніж x , Що відбувається в 50% випадків, оскільки x і y знаходяться у різних вихідних файлах.


Є багато рішень для цієї проблеми, але одне дуже просте і переносний – замінити глобальний об'єкт Fred x , Глобальної функцією x() , Яка повертає об'єкт типу Fred за посиланням.

    // File x.cpp

#include “Fred.hpp”

Fred& x()
{
static Fred* ans = new Fred();
return *ans;
}


Оскільки локальні статичні об'єкти створюються в момент, коли програма в процесі роботи у перший раз проходить через точку їх оголошення, інструкція new Fred() у прикладі вище буде виконана тільки один раз: під час першого виклику функції x() . Кожен наступний виклик поверне той же самий об'єкт Fred (Той, на який вказує ans ). І далі всі випадки використання об'єкта x замініть на виклики функції x() :

    // File Barney.cpp
#include “Barney.hpp”

Barney::Barney()
{
// …
x().goBowling();
// …
}


Це і називається "створення при першому використанні", глобальний об'єкт Fred створюється при першому зверненні до нього.


Негативним моментом цієї техніки є той факт, що об'єкт Fred ніде не знищується. Книга C + + FAQ Book описує додаткову техніку, яка дозволяє вирішити і цю проблему (правда, ціною появи можливий помилок порядку статичної деініціалізацію).


Примітка: помилки статичної ініціалізації не поширюються на базові / вбудовані типи, такі як int або char* . Наприклад, якщо ви створюєте статичну змінну типу float , У вас не буде проблем з порядком ініціалізації. Проблема виникає лише тоді, коли у вашого статичного чи глобального об'єкта є конструктор.


[10.13] Як боротися з помилками порядку статичної ініціалізації об'єктів – членів класу?


Використовуйте ту ж саму техніку, яка описана в [10.12], але замість глобальної функції використовуйте статичну функцію-член.


Припустимо, у вас є клас X , В якому є статичний об'єкт Fred :

    // File X.hpp

class X {
public:
// …

private:
static Fred x_;
};


Природно, цей статичний член ініціалізується окремо:

    // File X.cpp

#include “X.hpp”

Fred X::x_;


Знову ж природно, об'єкт Fred буде використаний в одному або декількох методах класу X :

    void X::someMethod()
{
x_.goBowling();
}

Проблема проявиться, якщо хтось десь будь-яким чином викличе цей метод, до того як об'єкт Fred буде створений. Наприклад, якщо хтось створює статичний об'єкт X і викликає його someMethod() під час статичної ініціалізації, то ваша доля цілком знаходиться в руках компілятора, який або створить X::x_ , До того як буде викликаний someMethod() , Або ж тільки після.


(Мушу зауважити, що ANSI / ISO комітет по C + + працює над цією проблемою, але компілятори, які працюють у відповідності з останніми змінами, поки недоступні, можливо, в майбутньому в цьому розділі будуть зроблено доповнення у зв'язку із зміною ситуації.)


У будь-якому випадку, завжди можна зберегти переносимість (і це абсолютно безпечний метод), замінивши статичний член X::x_ на статичну функцію-член:

    // File X.hpp

class X {
public:
// …

private:
static Fred& x();
};


Природно, цей статичний член ініціалізується окремо:

    // File X.cpp

#include “X.hpp”

Fred& X::x()
{
static Fred* ans = new Fred();
return *ans;
}


Після чого ви просто міняєте всі x_ на x() :

    void X::someMethod()
{
x().goBowling();
}

Якщо для вас надзвичайно важлива швидкість роботи програми і вас турбує необхідність додаткового виклику функції для кожного виклику X::someMethod() , То ви можете зробити static Fred& . Як ви пам'ятаєте, статичні локальні змінні инициализируются тільки один раз (при першому проходженні програми через їх оголошення), так що X::x() тепер буде викликана тільки один раз: під час першого виклику X::someMethod() :

    void X::someMethod()
{
static Fred& x = X::x();
x.goBowling();
}

Примітка: помилки статичної ініціалізації не поширюються на базові / вбудовані типи, такі як int або char* . Наприклад, якщо ви створюєте статичну змінну типу float , У вас не буде проблем з порядком ініціалізації. Проблема виникає лише тоді, коли у вашого статичного чи глобального об'єкта є конструктор.


[10.14] Як мені обробити помилку, яка сталася в конструкторі?


Згенеруйте виняток. Дивіться подробиці у [17.1].


Розділ [11]: Деструктори


[11.1] Що таке деструктор?


Деструкція – це виконання останньої волі об'єкта.


Деструктори використовуються для вивільнення зайнятих об'єктом ресурсів. Наприклад, клас Lock може заблокувати ресурс для ексклюзивного використання, а його деструктор цей ресурс звільнити. Але самий частий випадок – це коли в конструкторі використовується new , А в деструкції – delete .


Деструктор це функція "готуйся до смерті". Часто слово деструктор скорочується до dtor.


[11.2] В якому порядку викликаються деструктори для локальних об'єктів?


У порядку зворотному тому, в якому ці об'єкти створювалися: першим створений – останнім буде знищений.


У наступному прикладі деструктор для об'єкта b буде викликаний першим, а тільки потім деструктор для об'єкта a :

    void userCode()
{
Fred a;
Fred b;
// …
}

[11.3] В якому порядку викликаються деструктори для масивів об'єктів?


У порядку зворотному створення: першим створений – останнім буде знищений.


У наступному прикладі порядок виклику деструкторів буде таким: a[9] , a[8] , …, a[1] , a[0] :

    void userCode()
{
Fred a[10];
// …
}

[11.4] Чи можу я перевантажити деструктор для свого класу?


Ні.


У кожного класу може бути тільки один деструктор. Для класу Fred він завжди буде називатися Fred::~Fred() . У деструктор ніколи не передається ніяких параметрів, і сам деструктор ніколи нічого не повертає.


Все одно ви не змогли б вказати параметри для деструктора, тому що ви ніколи на викликаєте деструктор безпосередньо [11.5] (точніше, майже ніколи [11.10]).


[11.5] Чи можу я явно викликати деструктор для локальної змінної?


Ні!


Деструктор все одно буде викликаний ще раз при досягненні закриває фігурної дужки } кінця блоку, в якому була створена локальна змінна. Цей виклик гарантується мовою, і він відбувається автоматично, нема способу цей виклик запобігти. Але наслідки повторного виклику деструктора для одного і того ж об'єкта можуть бути плачевними. Бах! І ви небіжчик …


[11.6] А що якщо я хочу, щоб локальна змінна "померла" раніше закриває фігурної дужки? Чи можу я при крайній необхідності викликати деструктор для локальної змінної?


Ні! [Дивіться відповідь на попереднє питання [11.5]].


Припустимо, що (бажаний) побічний ефект від виклику деструктора для локального об'єкта File полягає у закритті файлу. І припустимо, що у нас є екземпляр f класу File і ми хочемо, щоб файл f був закритий раніше кінця свого області видимості (тобто, раніше } ):

    void someCode()
{
File f;

/ / … [Цей код виконується при відкритому f] …

/ / <- Нам потрібен ефект деструктора f тут

 / / … [Цей код виконується після закриття f] …
    } 

Для цієї проблеми є просте рішення, яке ми покажемо в [11.7]. Але поки запам'ятайте тільки наступне: не можна явно викликати деструктор [11.5].


[11.7] Добре, я не буду явно викликати деструктор. Але як мені впоратися з цією проблемою?


[Також дивіться відповідь на попереднє питання [11.6]].


Просто помістіть вашу локальну змінну в окремий блок {…} , Відповідний необхідному часу життя цієї змінної:

    void someCode()
{
{
File f;
/ / … [У цьому місці f ще відкрите] …
}
/ / ^ – Деструктор f буде автомагіческі викликаний тут!

/ / … [У цьому місці f вже буде закрито] …
}


[11.8] А що робити, якщо я не можу помістити змінну в окремий блок?


У більшості випадків ви можете скористатися додатковим блоком {…} для обмеження часу життя вашої змінної [11.7]. Але якщо з якоїсь причини ви не можете додати блок, додайте функцію-член, яка буде виконувати ті ж дії, що і деструктор. Але пам'ятайте: ви не можете самі викликати деструктор!


Наприклад, у випадку з класом File , Ви можете додати метод close() . Звичайний деструктор буде викликати close() . Зверніть увагу, що метод close() повинен буде якось відзначати об'єкт File , З тим щоб наступні виклики не намагалися закрити вже закритий файл. Наприклад, можна встановлювати змінну-член fileHandle_ в яке-небудь невикористовуване значення, типу -1, і перевіряти на початку, чи не містить fileHandle_ значення -1.

    class File {
public:
void close();
~File();
// …
private:
int fileHandle_; / / fileHandle_> = 0 якщо / тільки якщо файл відкрито
};

File::~File()
{
close();
}

void File::close()
{
if (fileHandle_ >= 0) {
/ / … [Викликати системну функцію для закриття файлу] …
fileHandle_ = -1;
}
}


Зверніть увагу, що іншим методам класу File теж може знадобитися перевіряти, чи не встановлений fileHandle_ в -1 (тобто, не закритий файл).


Також зверніть увагу, що всі конструктори, які не відкривають файл, повинні встановлювати fileHandle_ в -1.


[11.9] А чи можу я явно викликати деструктор для об'єкта, створеного за допомогою new?


Швидше за все, немає.


За винятком того випадку, коли ви використовували синтаксис розміщення для оператора new [11.10], вам слід просто видаляти об'єкти за допомогою delete , А не викликати явно деструктор. Припустимо, що ви створили об'єкт за допомогою звичайного new :

Fred* p = new Fred();

У такому випадку деструктор Fred::~Fred() буде автомагіческі викликаний, коли ви видаляєте об'єкт:

 delete p; / / Викликає p-> ~ Fred ()

Вам не слід явно викликати деструкцію, оскільки цим ви не звільняєте пам'ять, виділену для об'єкта Fred . Пам'ятайте: delete p робить відразу дві речі [16.8]: викликає деструктор і звільняє пам'ять.


[11.10] Що таке "синтаксис розміщення" new ("placement new") і навіщо він потрібний?


Є багато випадків для використання синтаксису розміщення для new . Найпростіше – ви можете використовувати синтаксис розміщення для приміщення об'єкта в певне місце в пам'яті. Для цього ви вказуєте місце, передаючи покажчик на нього в оператор new :

 # Include <new> / / Необхідно для використання синтаксису розміщення
# Include "Fred.h" / / Визначення класу Fred

void someCode()
{
char memory[sizeof(Fred)]; // #1
void* place = memory; // #2

Fred * f = new (place) Fred (); / / # 3 (дивіться "НЕБЕЗПЕКА" нижче)
/ / Покажчики f і place будуть рівні

// …
}


У рядку # 1 створюється масив з sizeof(Fred) байт, розмір якого достатній для зберігання об'єкта Fred . У рядку # 2 створюється покажчик place , Який вказує на перший байт масиву (досвідчені програмісти на С напевно помітять, що можна було і не створювати цей покажчик; ми це зробили лише щоб код був більш зрозумілим [As if -:) YM]). У рядку # 3 фактично відбувається тільки виклик конструктора Fred::Fred() . Покажчик this в конструкторі Fred буде дорівнює вказівником place . Таким чином, повертається покажчик теж буде дорівнює place .


РАДА: Не використовуйте синтаксис розміщення new , За винятком тих випадків, коли вам дійсно потрібно, щоб об'єкт був розміщений у певному місці в пам'яті. Наприклад, якщо у вас є апаратний таймер, відображений на певну ділянку пам'яті, то вам може знадобитися помістити об'єкт Clock за цією адресою.


НЕБЕЗПЕЧНО: Використовуючи синтаксис розміщення new ви берете на себе всю відповідальність за те, що передаваний вами покажчик вказує на достатній для зберігання об'єкта ділянку пам'яті з тим вирівнюванням (alignment), яке необхідне для вашого об'єкта. Ні компілятор, ні бібліотека не будуть перевіряти коректність ваших дій у цьому випадку. Якщо ваш клас Fred повинен бути вирівняний чотирьохбайтові кордоні, але ви передали до new покажчик на не вирівняний ділянку пам'яті, у вас можуть бути великі неприємності (якщо ви не знаєте, що таке "вирівнювання" (alignment), будь ласка, не використовуйте синтаксис розміщення new ). Ми вас попередили.


Також на вас лягає вся відповідальність за знищення розміщеного об'єкта. Для цього вам необхідно явно викликати деструктор:

    void someCode()
{
char memory[sizeof(Fred)];
void* p = memory;
Fred* f = new(p) Fred();
// …
f-> ~ Fred (); / / Явний виклик деструктора для розміщеного об'єкта
}

Це практично єдиний випадок, коли вам потрібно явно викликати деструктор.


[11.11] Коли я пишу деструктор, чи повинен я явно викликати деструктори для об'єктів-членів мого класу?


Ні. Ніколи не треба явно викликати деструктор (за винятком випадку з синтаксисом розміщення new [ 11.10 ]).


Деструкція класу (неявний, створений компілятором, або явно описаний вами) автоматично викликає деструктори об'єктів-членів класу. Ці об'єкти знищуються в порядку зворотному порядку їх оголошення в тілі класу:

    class Member {
public:
~Member();
// …
};

class Fred {
public:
~Fred();
// …
private:
Member x_;
Member y_;
Member z_;
};

Fred::~Fred()
{
/ / Компілятор автоматично викликає z_. ~ Member ()
/ / Компілятор автоматично викликає y_. ~ Member ()
/ / Компілятор автоматично викликає x_. ~ Member ()
}


[11.12] Коли я пишу деструктор похідного класу, чи потрібно мені явно викликати деструктор предка?


Ні. Ніколи не треба явно викликати деструктор (за винятком випадку з синтаксисом розміщення new [ 11.10 ]).


Деструктор похідного класу (неявний, створений компілятором, або явно описаний вами) автоматично викликає деструктори предків. Предки знищуються після знищення об'єктів-членів похідного класу. У випадку множинного успадкування безпосередні предки класу знищуються в порядку зворотному порядку їх появи в списку наслідування.

    class Member {
public:
~Member();
// …
};

class Base {
public:
virtual ~ Base (); / / Віртуальний деструктор [20.4]
// …
};

class Derived : public Base {
public:
~Derived();
// …
private:
Member x_;
};

Derived::~Derived()
{
/ / Компілятор автоматично викликає x_. ~ Member ()
/ / Компілятор автоматично викликає Base:: ~ Base ()
}


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


Кінець.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*