LINUX.ORG.RU

Управление моментом разрушения объекта

 


2

7

Привет. Нужно - задать необходимый порядок вызова деструкторов. Рабочий ли вариант:

struct S {
   const char *c;
   S(const char *c): c{c} {}
   ~S() {cout << c << endl;}
};

S &g1() {
   static S s{"g1"};
   return s;
}
S &g2() {
   static S s{"g2"};
   return s;
}
int main() {
   g1(); // создаём g1::S
   g2(); // создаём g2::S
   return 0;
}
Т.е создаю g1::S раньше g2::S, деструкторы в обратном порядке. Что смущает: может ли компилятор при каких-нибудь (не запрещено ли ему в принципе?) условиях заооптимизировать бесполезный на его взгляд вызов g1()/g2()?

Ответ на: комментарий от imatveev13

If multiple threads attempt to initialize the same static local variable concurrently, the initialization occurs exactly once (similar behavior can be obtained for arbitrary functions with std::call_once). Note: usual implementations of this feature use variants of the double-checked locking pattern, which reduces runtime overhead for already-initialized local statics to a single non-atomic boolean comparison.

Первый поток, который пройдёт через объявление инициализирует. И это:

If the completion of the constructor or dynamic initialization for thread-local or static object A was sequenced-before thread-local or static object B, the completion of the destruction of B is sequenced-before the start of the destruction of A

pavlick ()
Последнее исправление: pavlick (всего исправлений: 1)
Ответ на: комментарий от xaizek

Спасибо. Но я уже засомневался в таком коде вот по какой причине: не всегда инициализация static объекта в block scope будет происходить при первом входе потока в функции, и весь ожидаемый мной порядок полетит.

Variables declared at block scope with the specifier static have static storage duration but are initialized the first time control passes through their declaration (unless their initialization is zero- or constant-initialization, which can be performed before the block is first entered).

т.е. если объект с тривиальным дефолтным конструктором и инициализация вида T obj{} (при далеко нетривиальном деструкторе), то привет UB. Не хочется полагаться на способ, который там работает, а вон там нет.

pavlick ()
Ответ на: комментарий от pavlick

Хотя, возможно при таком переносе инициализации на «пораньше» всё будет соблюдён порядак как в случае без такой оптимизации, но у меня нет никакой инормации об этом, не очень освещённый нюанс.

pavlick ()
Ответ на: комментарий от pavlick

[basic.start.term]

... If the completion of the constructor or dynamic initialization of an object with static storage duration is sequenced before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first. [ Note: This definition permits concurrent destruction. —end note ] ...

но ты хочешь странного. зачем тебе понадобились глобальные ссылки на локальные статические переменные?

anonymous ()

Стандарт какой? Если 17, то лучше просто

inline S g1{"g1"};
inline S g2{"g2"};
прямо в хедере. Порядок вызова деструкторов будет обратный (g2, потом g1).

// может поможет https://www.reddit.com/r/cpp/comments/79c1pn/cppcon_2017_nir_friedman_what_c_...

d_a ★★★★★ ()
Последнее исправление: d_a (всего исправлений: 1)
Ответ на: комментарий от anonymous

Синглтоны. Будут давать ссылку на шибко замудрённые сущности, которые будут дёргать другие синглтоны. Естественно, мне нужен порядок, который гарантирует, что что после смерти нижележащего никому не захочется что-то от него получить. Как реализовать иначе я знаю, но хотелось так (возврат ссылки на статик).

pavlick ()
Ответ на: комментарий от pavlick

синглтоны -- это плохо... п-нятненько?

этот вопрос исчерпывающе разобран у александреску. добавить к нему особо нечего.

не люблю учить плохому, но если ты настаиваешь... собери все синглтоны в программе в один класс. и только его и сделай синглтоном.

anonymous ()
Ответ на: комментарий от xaizek

Спасибо, похоже вы разгадали эту головоломку )). Чуть позже покопаю сгенерированные асм инструкции (наличие оптимизации), убедиться, что вызов деструкторов не затронут. Отпишусь.

pavlick ()
Ответ на: комментарий от pavlick

Вроде так и работает. Переменные внизу в секции данных, деструкторы сохраняются через __cxa_atexit. Правда __cxa_atexit вызывается после __cxa_guard_release, это видимо оптимизация (как-то кажется, что так нельзя делать, но может им виднее).

xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

гадание по ассемблерному выхлопу :)

почему нельзя? __cxa_guard_* защищает только флаг того, был ли вызван конструктор.

как это отвечает на вопрос из оп-поста? естественно, с++ гарантирует, что деструкторы будут вызваны в порядке, обратном вызову конструкторов. вопрос в том, в каком порядке вызовутся конструкторы статических объектов. гарантий нет даже для локальных.

anonymous ()
Ответ на: гадание по ассемблерному выхлопу :) от anonymous

почему нельзя? __cxa_guard_* защищает только флаг того, был ли вызван конструктор.

Потому что, если будет разблокирован поток, который потом приведёт к завершению работы приложения, то деструктор нового объекта может быть не вызван так как ещё не зарегистрирован.

вопрос в том, в каком порядке вызовутся конструкторы статических объектов.

При первом заходе в блок или никогда, как по стандарту.

гарантий нет даже для локальных.

Сверху-вниз.

xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

если будет разблокирован поток, который потом приведёт к завершению работы приложения

любой другой поток может привести к завершению работы приложения в любой момент и без всяких блокировок

При первом заходе в блок или никогда, как по стандарту.

вот «никогда» как раз и интересно. в каком порядке вызовутся деструкторы?

Сверху-вниз.

сверху чего? неплохо бы ссылку

anonymous ()
Ответ на: комментарий от anonymous

любой другой поток может привести к завершению работы приложения в любой момент и без всяких блокировок

А, ну да, может по этому эта оптимизация и разрешена, так как хуже не сделает в общем случае.

вот «никогда» как раз и интересно. в каком порядке вызовутся деструкторы?

Если цитата выше трактована правильно, то конструктор как бы вызовется, когда и должен был, десктуктор будут запланирован тогда же.

сверху чего? неплохо бы ссылку

В порядке объявления в коде. http://eel.is/c draft/basic.start.dynamic#2.1

xaizek ★★★★★ ()
Ответ на: комментарий от xaizek

Предположение верно, я читал доки по std::exit https://en.cppreference.com/w/cpp/utility/program/exit, вполне ясно это описано:

if the compiler opted to lift dynamic initialization of an object to the static initialization phase of non-local initialization, the sequencing of destruction honors its would-be dynamic initialization.

Так что, думаю, код из первого сообщения вполне валидный. Благодарю всех за участие.

pavlick ()
Ответ на: комментарий от xaizek

Если цитата выше трактована правильно, то конструктор как бы вызовется, когда и должен был, десктуктор будут запланирован тогда же.

я такой вывод из этой цитаты сделать не смог.

В порядке объявления в коде. http://eel.is/c draft/basic.start.dynamic#2.1

Это же про "Dynamic initialization of non-local variables"

насчёт статической инициализации стандарт предельно лаконичен:

Together, zero-initialization and constant initialization are called static initialization; all other initialization is dynamic initialization. Static initialization shall be performed before any dynamic initialization takes place.

и действительно, о чём здесь писать, если инициализация происходит во время компиляции.

но есть и иинтересная оговорка:

An implementation is permitted to perform the initialization of a non-local variable with static storage duration as a static initialization even if such initialization is not required to be done statically, provided that

— the dynamic version of the initialization does not change the value of any other object of namespace scope prior to its initialization, and

— the static version of the initialization produces the same value in the initialized variable as would be produced by the dynamic initialization if all variables not required to be initialized statically were initialized dynamically.

[ Note: As a consequence, if the initialization of an object obj1 refers to an object obj2 of namespace scope potentially requiring dynamic initialization and defined later in the same translation unit, it is unspecified whether the value of obj2 used will be the value of the fully initialized obj2 (because obj2 was statically initialized) or will be the value of obj2 merely zero-initialized.

с динамической инициализацией всё тоже оказывается не очень просто. кроме того, что порядок для разных модулей трансляции не определён...

Dynamic initialization of a non-local variable with static storage duration is either ordered or unordered.

не буду постить всю простыню условий для unordered (касаются шаблонов и потоков). предлагаю самостоятельно ознакомиться в разделе [basic.start.init]

дальше к собственно, локальным статическим переменным (внезапно, в разделе [stmt.dcl]), которые для большинства случаев ссылаются на нелокальные (поэтому я разобрал их выше). просто вишенка на торте:

If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration.

для полноты картины заглянем в раздел [basic.start.term]. он тоже предсказуемо лаконичен:

If an object is initialized statically, the object is destroyed in the same order as if the object was dynamically initialized.

это вселяет некоторую надежду, но учитывая всё вышесказанное, я, всё-таки, встану на сторону александреску (даже с учётом изменений в с++11): порядок инициализации статических объектов всё равно остаётся лотереей.

anonymous ()

Ну и в качестве небольшого теста. Погонял под отладчиком, компилировал с -O2.

struct S {
   int i;
   ~S() {}
};
S &g1_my() {
   static S s{}; return s;
}
S &g2_my() {
   static S s{}; return s;
}
int main() {
#ifdef FIRST
   g1_my(); g2_my();
#else
   g2_my(); g1_my();
#endif
   return 0;
}
Порядок вызова деструкторов (не заоптимизировались):
#defind FIRST
//g2_my()::s
//g1_my()::s
#defind SECOND
//g1_my()::s
//g2_my()::s

pavlick ()
Последнее исправление: pavlick (всего исправлений: 1)
Ответ на: удивительно, правда? от anonymous

Пример такой (простой деструктор без side эффектов, возможность перенести инициализацию до static c dynamic для того, чтобы дать возможность заоптимизировать всё и поломать логику. Если покажите пример, где работать не будет (но не многострочную простыню кода), то с интересом покопаю.

pavlick ()
Ответ на: комментарий от pavlick

лень писать код, но короткий пример будет содержать выбрасывание исключения из конструктора. например, на первый вызов.

в рамках одной единицы трансляции, да и вообще небольшого приложения для локалхоста, такие эффекты не рассматривать.

anonymous ()

Важно отметить, что если объект не был ODR-used, то компилятор спокойно вырежит инициализацию не смотря даже на наличие side эффектов

#include <iostream>
using namespace std;

struct S{
   S() {cout << "S::ctr" << endl;}
}thread_local s;

int a;

int main() {
   &s;   // №1
   &a;   // №2
   return 0;
}
Если закомментировать №1, то cout не отработает. Стандарт по этому поводу говорит:

Deferred dynamic initialization

It is implementation-defined whether dynamic initialization happens-before the first statement of the main function (for statics) or the initial function of the thread (for thread-locals), or deferred to happen after.

If the initialization of a non-inline variable (since C++17) is deferred to happen after the first statement of main/thread function, it happens before the first odr-use of any variable with static/thread storage duration defined in the same translation unit as the variable to be initialized. If no variable or function is odr-used from a given translation unit, the non-local variables defined in that translation unit may never be initialized (this models the behavior of an on-demand dynamic library). However, as long as anything from a translation unit is odr-used, all non-local variables whose initialization or destruction has side effects will be initialized even if they are not used in the program.

Тут как-то странно, вроде взятие адреса с a должно заставить инициализироваться s, но нет.

pavlick ()