Форум программистов, компьютерный форум, киберфорум
C++
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
 
Рейтинг 4.85/13: Рейтинг темы: голосов - 13, средняя оценка - 4.85
2 / 2 / 0
Регистрация: 16.08.2013
Сообщений: 86

Как избежать использования разрушенной переменной?

16.11.2023, 02:12. Показов 2542. Ответов 27
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Никак не могу найти приемлимое решение следующей проблемы.

Допустим, мы имеем логирующую функцию log(), которая может быть вызвана из любого места программы. Например:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <mutex>
 
void log(const char* s);
 
class A {
public:
    A() { }
    ~A() { log("~A"); }
} a;
 
class B {
public:
    B() { log("B"); }
    ~B() { }
} b;
 
void log(const char* s) {
    static std::shared_ptr<std::mutex> m = std::make_shared<std::mutex>();
    std::unique_lock<std::mutex> lock(*m);
    std::cout << s << "\n";
}
 
 
int main() {
    return 0;
}
Проблема в том, что второй по времени вызов log(), который произойдёт из ~A(), приведёт к неопределённому поведению или крашу, так как статическая переменная "m" уже разрушена. Как выходить из таких ситуаций?

Вижу следующие варианты, которые мне не совсем нравятся:
* Не создавать в деструкторах зависимость от других глобальных переменных. В данном примере - не вызывать log() из ~A(). Но ведь можно представить ситуацию, что в деструкторе ожидается завершение параллельного потока, который как раз может использовать log()...
* Переместить в самый верх кода подобные глобальные переменные, делая их последними в очереди на разрушение. В данном примере - переместить "m" наверх. Но тогда реализация функции log() будет разделена на 2 части...
0
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
16.11.2023, 02:12
Ответы с готовыми решениями:

Как избежать использования оператора goto?
typedef struct BOOK { char *name; } book; int main() { FILE *fp; char *fname = &quot;sometxt&quot;; book nbook; ...

Как избежать использования кучи проверок?
В общем есть файл конфига, в котором хранятся переменные, так вот, я хотел бы узнать, можно ли избежать кучи проверок, ведь при загрузке...

Как избежать утечки памяти: нюансы использования QList
class T { QList&lt;bitset&lt;32&gt; &gt; bits; QList&lt;bitset&lt;32&gt; &gt; newList() { return QList&lt;bitset&lt;32&gt; &gt;(); } void...

27
7804 / 6568 / 2988
Регистрация: 14.04.2014
Сообщений: 28,705
16.11.2023, 10:21
Цитата Сообщение от Paket236 Посмотреть сообщение
Но ведь можно представить ситуацию, что в деструкторе ожидается завершение параллельного потока, который как раз может использовать log()
Потоки должны завершиться до return. Разве нет?
0
фрилансер
 Аватар для Алексей1153
6454 / 5655 / 1129
Регистрация: 11.10.2019
Сообщений: 15,054
16.11.2023, 10:32
Цитата Сообщение от Paket236 Посмотреть сообщение
так как статическая переменная "m" уже разрушена.
с чего вдруг?

Добавлено через 1 минуту
Цитата Сообщение от Paket236 Посмотреть сообщение
static std::shared_ptr<std::mutex> m = std::make_shared<std::mutex>();
а зачем мутекс делать шаред?
0
19497 / 10102 / 2461
Регистрация: 30.01.2014
Сообщений: 17,808
16.11.2023, 10:35
Цитата Сообщение от Paket236 Посмотреть сообщение
Переместить в самый верх кода подобные глобальные переменные, делая их последними в очереди на разрушение. В данном примере - переместить "m" наверх. Но тогда реализация функции log() будет разделена на 2 части...
Это ничего не даст, если в проекте появятся глобальные объекты в разных файлах. Порядок создания и уничтожения объектов в разных ЕТ не определен.

Цитата Сообщение от Paket236 Посмотреть сообщение
Не создавать в деструкторах зависимость от других глобальных переменных. В данном примере - не вызывать log() из ~A(). Но ведь можно представить ситуацию, что в деструкторе ожидается завершение параллельного потока, который как раз может использовать log()...
Тут скорее нужно избавляться от самих глобальных переменных.
0
7804 / 6568 / 2988
Регистрация: 14.04.2014
Сообщений: 28,705
16.11.2023, 10:37
DrOffset, а конкретно в его случае static в функции когда уничтожается? Просто в общем порядке обявления с этими a, b? Это вообще как-то регламентировано?
0
19497 / 10102 / 2461
Регистрация: 30.01.2014
Сообщений: 17,808
16.11.2023, 10:57
Цитата Сообщение от nmcf Посмотреть сообщение
Это вообще как-то регламентировано?
Регламентировано в пределах одной ЕТ.

Цитата Сообщение от nmcf Посмотреть сообщение
DrOffset, а конкретно в его случае static в функции когда уничтожается?
Уничтожается в порядке, обратном созданию.

У него в коде сначала будет создание объекта a, затем посредством вызова функции создастся локальный статический объект m, потом объект b.

Добавлено через 10 минут
Не смотря на то, что ТС скорее всего это не понравится, единственное нормальное решение здесь - это избавляться от глобальных объектов совсем, если в их деструкторах предполагается такая сложная взаимологика.
1
7804 / 6568 / 2988
Регистрация: 14.04.2014
Сообщений: 28,705
16.11.2023, 11:38
DrOffset, с созданием понятно, а уничтожение? Не очевидно, что это зависит от положения функции в файле.
0
19497 / 10102 / 2461
Регистрация: 30.01.2014
Сообщений: 17,808
16.11.2023, 12:06
Цитата Сообщение от nmcf Посмотреть сообщение
с созданием понятно, а уничтожение? Не очевидно, что это зависит от положения функции в файле.
Я же написал
Цитата Сообщение от DrOffset Посмотреть сообщение
Уничтожается в порядке, обратном созданию.
Добавлено через 2 минуты
Цитата Сообщение от nmcf Посмотреть сообщение
Не очевидно, что это зависит от положения функции в файле.
Не от положения функций, а от порядка создания.
Порядок создания глобальных объектов зависит от порядка их объявления в файле. А локальный статический объект создается в момент первого обращения. Если вы написали такой код, который вклинивает первое обращение между созданием двух глобальных объектов, ну что ж, значит так тому и быть: уничтожение будет в порядке обратном созданию. Потому что у всех этих объектов один и тот же storage duration.
1
2 / 2 / 0
Регистрация: 16.08.2013
Сообщений: 86
16.11.2023, 15:23  [ТС]
Цитата Сообщение от nmcf Посмотреть сообщение
Потоки должны завершиться до return. Разве нет?
Тоже так думал раньше... Но нигде не находил подтверждение данному предположению, везде подразумевались деструкторы для завершения потоков. Да, завершение потоков "до return" упрощает решение некоторых проблем, но одна из них воспроизводится в том числе и в однопотоке и описана она в первом посте... Поэтому я засомневался.

Цитата Сообщение от Алексей1153 Посмотреть сообщение
с чего вдруг?
Если пройтись пошагово в отладчике Visual Studio 2019, то это видно.

Цитата Сообщение от Алексей1153 Посмотреть сообщение
а зачем мутекс делать шаред?
Как раз для того, чтобы это было видно и можно было воспроизвести краш.

Цитата Сообщение от DrOffset Посмотреть сообщение
Тут скорее нужно избавляться от самих глобальных переменных.
На самом деле, можно провести аналогию не с глобальными переменными, а с полями класса. Хотя, здесь, кажется, придумать что-то можно. Например, заменить тип проблемного поля на std::shared_ptr и скопировать его туда, где оно может использоваться во время разрушения полей (например, в параллельный поток, который должен завершиться в одном из деструкторов одного из полей), тем самым продлив жизнь данным.

Цитата Сообщение от DrOffset Посмотреть сообщение
У него в коде сначала будет создание объекта a, затем посредством вызова функции создастся локальный статический объект m, потом объект b.
Да, так и есть.

Цитата Сообщение от DrOffset Посмотреть сообщение
Не смотря на то, что ТС скорее всего это не понравится, единственное нормальное решение здесь - это избавляться от глобальных объектов совсем, если в их деструкторах предполагается такая сложная взаимологика.
Тогда буду думать в этом направлении. Спасибо за совет! Если других предложений не будет, то помечу данный пост как решение.
0
фрилансер
 Аватар для Алексей1153
6454 / 5655 / 1129
Регистрация: 11.10.2019
Сообщений: 15,054
16.11.2023, 16:49
Цитата Сообщение от Paket236 Посмотреть сообщение
Если пройтись пошагово в отладчике Visual Studio 2019, то это видно.
да, я уже понял, почему это возможно. Из ответе № 8 Но это нужно постараться
0
19497 / 10102 / 2461
Регистрация: 30.01.2014
Сообщений: 17,808
16.11.2023, 16:59
Цитата Сообщение от Paket236 Посмотреть сообщение
Если других предложений не будет
Paket236, есть решение, правда оно зависит от вариантов использования. Это решение описано в книге Александреску "Современное проектирование на C++". Называется синглтон "Феникс".
Его суть в том, что объект может быть уничтожен, но синглтон написан так, что "возрождает" его, если происходит обращение к "трупу" .
Для вашего класса "логгер" может подойти, но если требуется сохранять состояние от одной жизни к другой, то могут возникнуть сложности.

В любом случае это решение "не очень", потом что архитектурно оно кривое. Избавление от глобальных взаимосвязей гораздо более надежная вещь.
1
7804 / 6568 / 2988
Регистрация: 14.04.2014
Сообщений: 28,705
16.11.2023, 17:25
Вроде бы нашёл в разделе Termination. Витиевато написано.

If the completion of the constructor or dynamic initialization of an object with static storage duration strongly happens before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first. If the completion of the constructor or dynamic initialization of an object with thread 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.

Добавлено через 3 минуты

Не по теме:

Цитата Сообщение от Paket236 Посмотреть сообщение
Тоже так думал раньше...
Ты же сам их завершаешь. Значит, знаешь когда.

0
2 / 2 / 0
Регистрация: 16.08.2013
Сообщений: 86
16.11.2023, 19:29  [ТС]
Кажется, нашёл приемлемое решение.
Проблема описана здесь (не уверен, разрешены ли здесь ссылки на сторонние ресурсы):
1: How do I prevent the "static initialization order problem".
2: Why doesn’t the Construct On First Use Idiom use a static object instead of a static pointer?.

Смысл следующий. Чтобы контролировать порядок создания глобальной переменной, нужно обернуть её в функцию, дописать static (как это сделано в функции log() в моём примере) и вызвать в нужном месте. А чтобы гарантировать неразрушенность этой переменной в каком-нибудь "глобальном" деструкторе, нужно сделать пробный "создающий" вызов в соответствующем конструкторе. Таким образом, мой пример станет выглядеть вот так (изменилась лишь строка #8):

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <mutex>
 
void log(const char* s);
 
class A {
public:
    A() { log(""); }        // фикс
    ~A() { log("~A"); }
} a;
 
class B {
public:
    B() { log("B"); }
    ~B() { }
} b;
 
void log(const char* s) {
    static std::shared_ptr<std::mutex> m = std::make_shared<std::mutex>();
    std::unique_lock<std::mutex> lock(*m);
    std::cout << s << "\n";
}
 
 
int main() {
    return 0;
}
Если бы можно было все "создающие" вызовы (такие, как log()) разместить в каком-нибудь callback'е и передать его в OS-зависимую функцию, которая вызовет этот callback до main() и до создания всех глобальных переменных, было бы ещё лучше. Но я ничего такого при беглом гуглении не нашёл.
0
19497 / 10102 / 2461
Регистрация: 30.01.2014
Сообщений: 17,808
16.11.2023, 20:42
Цитата Сообщение от Paket236 Посмотреть сообщение
Кажется, нашёл приемлемое решение.
Ну то есть хорошее решение вам не нужно, нужно просто хоть какое-то?

Как я уже сказал, это решение не будет гарантированно работать, если глобальные объекты, использующие эту функцию, находятся в разных ЕТ. Т.е. в пределах одного файла ваше решение сработает, если файлов несколько - не факт.

Да, там по вашей ссылке есть еще один вариант решения: паттерн Nifty Counter, но вы что-то про него не сказали ничего, и ваш пример решением проблемы в целом не является.
0
2 / 2 / 0
Регистрация: 16.08.2013
Сообщений: 86
17.11.2023, 05:11  [ТС]
Цитата Сообщение от DrOffset Посмотреть сообщение
Как я уже сказал, это решение не будет гарантированно работать, если глобальные объекты, использующие эту функцию, находятся в разных ЕТ. Т.е. в пределах одного файла ваше решение сработает, если файлов несколько - не факт.
Я сначала не хотел спорить и остаться при своём... но всё же решил уточнить.
Можно как-то убедиться в этом утверждении? Потому как я не увидел где-то упоминаний (например, читая здесь и здесь), что данное решение может иметь проблемы при использовании разных единиц трансляции. Плюс, моя собственная проверка пока не подтвердила это.
0
 Аватар для eva2326
1673 / 501 / 107
Регистрация: 17.05.2015
Сообщений: 1,518
17.11.2023, 07:28
Цитата Сообщение от Paket236 Посмотреть сообщение
Как выходить из таких ситуаций?
Разрушение статических объектов происходит в обратном порядке по отношению к их созданию.
Соответственно, правило: если деструктор хочет позвать статический объект, тогда в конструкторе тоже нужно позвать этот же самый статический объект.

C++
1
2
3
4
5
class A {
public:
    A() { log("A");  }
    ~A() { log("~A"); }
} a;
Цитата Сообщение от Paket236 Посмотреть сообщение
static std::shared_ptr<std::mutex> m = std::make_shared<std::mutex>();
В вашем случае используется локальный статический объект.
Локальные статические объекты имеют иммунитет к "неопределенному порядку инициализации единиц трансляций"

Проблема тут может быть только в том случае, если два статических объекта перекрестно ссылаются друг на друга в конструкторах.
Но так как ваша функция log никак не зависит от класса A, то проблем никаких.

Цитата Сообщение от DrOffset Посмотреть сообщение
Как я уже сказал, это решение не будет гарантированно работать
Будет.

Пока конструктор полностью не завершил свою работу, объект считается ещё не созданным.
В момент работы конструктора объекта класса A
Цитата Сообщение от Paket236 Посмотреть сообщение
A() { log(""); }        // фикс
Вызов log("") происходит в момент, когда объект A ещё не построен.
Поэтому, локальный статический объект, принадлежащий функции log, гарантированно будет создан раньше, чем объект класса A

А дальше вступает в силу:
Цитата Сообщение от DrOffset Посмотреть сообщение
Уничтожается в порядке, обратном созданию.
На момент работы деструктора объекта класса A, локальный статический объект функции log ещё жив, потому что он был создан раньше, чем был создан объект класса А, и следовательно гарантированно проживет дольше, чем объект класса A
2
19497 / 10102 / 2461
Регистрация: 30.01.2014
Сообщений: 17,808
17.11.2023, 10:59
Цитата Сообщение от Paket236 Посмотреть сообщение
Можно как-то убедиться в этом утверждении?
Вот здесь в конце об этом говорится: https://isocpp.org/wiki/faq/ct... rst-use-v2

Суть проблемы не в порядке статической инициализации (ее вы как раз победили, разместив вызов в конструкторе, и выкладки автора выше в этом вопросе верны), а в статической деинициализации.
Эту проблему как раз решают паттерны "Nifty counter" и "Phoenix singleton".

Paket236, оратор выше в целом отвечал не на то, о чем я говорил.

Вот у вас есть три объекта в текущей ЕТ - а, b, c, объявленные друг за другом. Все они используют log в своих деструкторах. Вы смотрите, что объект А идет первым, поэтому добавляете туда (по вашему посту №13) "создающий вызов". Далее есть другая ЕТ, где объекты идут в другом составе, например, как b, c. Тогда и в конструктор B нужно тоже будет добавить такой создающий вызов. Если этого не сделать, не смотря на то, что есть ЕТ, где порядок верный, может случиться "фиаско", но уже в деструкторе B.
Т.е. таким образом абсолютно любой такой глобальный объект, где бы он не находился, должен делать такие создающие вызовы в конструкторе, что в общем-то достаточно неудобно, и требует постоянного контроля за новыми такими объектами в разных ЕТ.
Поэтому я и говорю, что это решение не будет гарантированно работать из-за особенностей компиляции C++ и присутствия человеческого фактора.
1
1459 / 475 / 70
Регистрация: 22.09.2023
Сообщений: 1,440
17.11.2023, 11:03
Вообще-то тут есть один момент, на который программисты больших компьютеров не обращают внимания:
Да, статический неконстантный объект внутри функции log создается в момент первого вызова функции. Но какой ценой это реализовано? А реализовано это в общем случае довольно просто: есть скрытый статический флаг "объект m создан", который инициализируется значением false перед вызовом конструкторов. Далее, при каждом вызове функции происходит проверка: а не равен ли этот флаг false и если равен - вызывается конструктор объекта и флаг переводится в состояние true. То есть действие, которое нужно сделать всего один раз тянет за собой код, который делает бесполезную работу при каждом вызове этой функции в течении всего остального времени жизни программы. Ну, и еще этот скрытый флаг занимает память.

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

Ну и это все относится также и к шаблону проектирования singleton, поскольку в нем точно такой же механизм реализован вручную.
1
19497 / 10102 / 2461
Регистрация: 30.01.2014
Сообщений: 17,808
17.11.2023, 11:23
Dushevny, начиная с С++11 этот флаг еще и атомарным обязан быть, что не добавляет к упомянутому производительности

И вообще я продолжаю настаивать на том, что наиболее хорошим решением будет избавление от глобальных объектов со сложной логикой в конструкторах и деструкторах совсем.
0
2 / 2 / 0
Регистрация: 16.08.2013
Сообщений: 86
17.11.2023, 11:51  [ТС]
DrOffset, спасибо за разъяснения, теперь понял вашу мысль. Я это подразумевал уже на тот момент, так что готов с этим смириться. Придётся теперь это учитывать.

eva2326, Dushevny, тоже поставил плюсы за ваши комментарии.
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
17.11.2023, 11:51
Помогаю со студенческими работами здесь

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

Как избежать глобальной переменной
Можно ли как-то так делать не применяя глобальных переменных procedure TForm1.Button1Click(Sender: TObject); var S:TStringList; ...

Можно ли избежать использования курсора?
Есть такие таблицы: CREATE TABLE . AS TABLE( NOT NULL, NULL, PRIMARY KEY CLUSTERED ( ASC )WITH (IGNORE_DUP_KEY =...

Избежать использования WildCard символов в Like запросах
Всем доброго времени суток. Возникла следующая трудность: спецсимволы % и _ в like запросе. Имеется mybatis маппер и следующий...

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


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
20
Ответ Создать тему
Новые блоги и статьи
SDL3 для Web (WebAssembly): Обработчик клика мыши в браузере ПК и касания экрана в браузере на мобильном устройстве
8Observer8 02.02.2026
Содержание блога Для начала пошагово создадим рабочий пример для подготовки к экспериментам в браузере ПК и в браузере мобильного устройства. Потом напишем обработчик клика мыши и обработчик. . .
Философия технологии
iceja 01.02.2026
На мой взгляд у человека в технических проектах остается роль генерального директора. Все остальное нейронки делают уже лучше человека. Они не могут нести предпринимательские риски, не могут. . .
SDL3 для Web (WebAssembly): Вывод текста со шрифтом TTF с помощью SDL3_ttf
8Observer8 01.02.2026
Содержание блога В этой пошаговой инструкции создадим с нуля веб-приложение, которое выводит текст в окне браузера. Запустим на Android на локальном сервере. Загрузим Release на бесплатный. . .
SDL3 для Web (WebAssembly): Сборка C/C++ проекта из консоли
8Observer8 30.01.2026
Содержание блога Если вы откроете примеры для начинающих на официальном репозитории SDL3 в папке: examples, то вы увидите, что все примеры используют следующие четыре обязательные функции, а. . .
SDL3 для Web (WebAssembly): Установка Emscripten SDK (emsdk) и CMake для сборки C и C++ приложений в Wasm
8Observer8 30.01.2026
Содержание блога Для того чтобы скачать Emscripten SDK (emsdk) необходимо сначало скачать и уставить Git: Install for Windows. Следуйте стандартной процедуре установки Git через установщик. . . .
SDL3 для Android: Подключение Box2D v3, физика и отрисовка коллайдеров
8Observer8 29.01.2026
Содержание блога Box2D - это библиотека для 2D физики для анимаций и игр. С её помощью можно определять были ли коллизии между конкретными объектами. Версия v3 была полностью переписана на Си, в. . .
Инструменты COM: Сохранение данный из VARIANT в файл и загрузка из файла в VARIANT
bedvit 28.01.2026
Сохранение базовых типов COM и массивов (одномерных или двухмерных) любой вложенности (деревья) в файл, с возможностью выбора алгоритмов сжатия и шифрования. Часть библиотеки BedvitCOM Использованы. . .
SDL3 для Android: Загрузка PNG с альфа-каналом с помощью SDL_LoadPNG (без SDL3_image)
8Observer8 28.01.2026
Содержание блога SDL3 имеет собственные средства для загрузки и отображения PNG-файлов с альфа-каналом и базовой работы с ними. В этой инструкции используется функция SDL_LoadPNG(), которая. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru