Поиск проблем с порядком статической инициализации C++

Мы столкнулись с некоторыми проблемами с фиаско с порядком статической инициализации, и я ищу способы прочесать большой объем кода, чтобы найти возможные вхождения. Есть предложения, как это сделать эффективно?

Обновлено: я получаю хорошие ответы о том, как РЕШИТЬ проблему статического порядка инициализации, но это не совсем мой вопрос. Я хотел бы знать, как НАЙТИ объекты, подверженные этой проблеме. Ответ Эвана кажется лучшим на данный момент в этом отношении; Я не думаю, что мы можем использовать valgrind, но у нас могут быть инструменты анализа памяти, которые могли бы выполнять аналогичную функцию. Это позволит выявить проблемы только в том случае, если порядок инициализации неправильный для данной сборки, и порядок может меняться с каждой сборкой. Возможно, есть инструмент статического анализа, который бы это уловил. Наша платформа - это компилятор IBM XLC / C++, работающий на AIX.

Плюс один за их поиск. Я подал запрос на добавление функции в GCC несколько лет назад, но из этого ничего не вышло.
jww 08.12.2015 00:22
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
62
1
55 396
12

Ответы 12

возможно, используйте valgrind, чтобы найти использование неинициализированной памяти. Лучшее решение "фиаско статического порядка инициализации" - использовать статическую функцию, которая возвращает экземпляр объекта, подобный этому:

class A {
public:
    static X &getStatic() { static X my_static; return my_static; }
};

Таким образом, вы получаете доступ к своему статическому объекту, вызывая getStatic, это гарантирует, что он инициализируется при первом использовании.

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

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

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

Johannes Schaub - litb 02.12.2008 23:55

@litb, C++ FAQ Lite, похоже, не согласен с вами по поводу порядка уничтожения. См. parashift.com/c++-faq-lite/ctors.html#faq-10.12. Вы хорошо знаете стандарт - есть ли в нем что-то, что доказывает, что Маршалл Клайн неправ? 8v)

Fred Larson 03.12.2008 00:09

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

Evan Teran 03.12.2008 00:17

@ Эван, я думаю, что в твоем примере что-то не так. Метод getStatic () возвращает значение, но, похоже, он не имеет ничего общего с элементом something_static. Есть ли у something_static цель?

Fred Larson 03.12.2008 00:28

Таким образом, вы получаете доступ к A :: something_static, вызывая getStatic << да, я тоже этого не понимаю: /. либо вы используете A :: something_static, либо вызываете getStatic (); и использовать то, что он возвращает, я думаю

Johannes Schaub - litb 03.12.2008 00:33

ООП! Я думал о двух разных вещах, когда напечатал это, теперь исправлено.

Evan Teran 03.12.2008 00:53

арг. пожалуйста, забудьте о моем комментарии выше. я напортачил. "если x в своем ctor вызывает y :: get (). foo (); тогда ctor y завершится раньше x 'ctor. Это означает, что y будет уничтожен до x, поскольку ctor y завершится раньше x». <на самом деле я имею в виду другое: y будет разрушено после x.

Johannes Schaub - litb 03.12.2008 02:09

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

Johannes Schaub - litb 03.12.2008 02:11

Замените все глобальные объекты глобальными функциями, которые возвращают ссылку на объект, объявленный статическим в функции. Это не является поточно-ориентированным, поэтому, если ваше приложение многопоточное, вам могут понадобиться некоторые уловки, такие как pthread_once или глобальная блокировка. Это гарантирует, что все будет инициализировано перед использованием.

Теперь либо ваша программа работает (ура!), Либо она находится в бесконечном цикле, потому что у вас есть циклическая зависимость (требуется редизайн), либо вы переходите к следующей ошибке.

Первое, что вам нужно сделать, это составить список всех статических объектов, имеющих нетривиальные конструкторы.

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

Шаблон singleton вызывает много критики, но ленивое построение "по мере необходимости" - довольно простой способ исправить большинство проблем сейчас и в будущем.

Старый...

MyObject myObject

новый...

MyObject &myObject()
{
  static MyObject myActualObject;
  return myActualObject;
}

Конечно, если ваше приложение многопоточное, это может вызвать у вас больше проблем, чем было изначально ...

Это не шаблон синглтона. У него похожая реализация, но это не одно и то же.

Martin York 03.12.2008 02:19

Скотт Мейерс сказал мне, что это было ;-) [эффективный C++, 2-е изд., Стр. 222]

Roddy 03.12.2008 14:32

Я называю приведенный выше паттерн «конструкция при первом использовании». Чтобы уточнить, шаблон singleton гарантирует, что у класса есть только один экземпляр, и обеспечивает глобальную точку доступа к нему. «Построить при первом использовании» - это / может быть частью, которая обеспечивает «глобальную точку доступа» для реализации синглтона. Однако сам по себе не гарантирует ни одного экземпляра.

Chris Bednarski 19.06.2011 05:59

Как это решает проблему? Дает ли это определенный порядок инициализации / деиницирования?

paulm 17.10.2014 13:21

@paulm - «построить при первом использовании» выше означает, что ваш объект не может быть использован до его создания. (типичная проблема - два глобальных объекта в отдельных единицах компиляции). И они уничтожаются в обратном порядке, так что да, порядок определен.

Roddy 17.10.2014 13:34

В зависимости от вашего компилятора вы можете разместить точку останова в коде инициализации конструктора. В Visual C++ это функция _initterm, которой даны указатели начала и конца списка функций, которые необходимо вызвать.

Затем перейдите к каждой функции, чтобы получить имя файла и функцию (при условии, что вы скомпилировали с включенной отладочной информацией). Когда у вас есть имена, выйдите из функции (вернитесь к _initterm) и продолжайте, пока _initterm не выйдет.

Это дает вам все статические инициализаторы, а не только те, которые есть в вашем коде - это самый простой способ получить исчерпывающий список. Вы можете отфильтровать те, которые не можете контролировать (например, в сторонних библиотеках).

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

Порядок решения инициализации:

Во-первых, это всего лишь временный обходной путь, потому что у вас есть глобальные переменные, от которых вы пытаетесь избавиться, но просто еще не успели (вы собираетесь избавиться от них в конце концов, не так ли? :-)

class A
{
    public:
        // Get the global instance abc
        static A& getInstance_abc()  // return a reference
        {
            static A instance_abc;
            return instance_abc;
        }
};

Это гарантирует, что он инициализируется при первом использовании и уничтожается при завершении работы приложения.

Многопоточная проблема:

C++ 11 делает гарантирует, что это потокобезопасный:

§6.7 [stmt.dcl] p4
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

Однако в C++ 03 нет официально гарантирует, что создание статических функциональных объектов является потокобезопасным. Так что технически метод getInstance_XXX() должен быть защищен критической секцией. С другой стороны, gcc имеет явный патч как часть компилятора, который гарантирует, что каждый объект статической функции будет инициализирован только один раз даже при наличии потоков.

Обратите внимание: Не использует дважды проверенный шаблон запирания, чтобы попытаться избежать затрат на блокировку. Это не будет работать в C++ 03.

Проблемы создания:

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

Проблемы разрушения:

Существует потенциальная проблема доступа к объекту после его уничтожения. Это происходит только в том случае, если вы обращаетесь к объекту из деструктора другой глобальной переменной (под глобальной я имею в виду любую нелокальную статическую переменную).

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

class B
{
    public:
        static B& getInstance_Bglob;
        {
            static B instance_Bglob;
            return instance_Bglob;;
        }

        ~B()
        {
             A::getInstance_abc().doSomthing();
             // The object abc is accessed from the destructor.
             // Potential problem.
             // You must guarantee that abc is destroyed after this object.
             // To guarantee this you must make sure it is constructed first.
             // To do this just access the object from the constructor.
        }

        B()
        {
            A::getInstance_abc();
            // abc is now fully constructed.
            // This means it was constructed before this object.
            // This means it will be destroyed after this object.
            // This means it is safe to use from the destructor.
        }
};

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

Fred Larson 03.12.2008 02:36

Блокировка с двойной проверкой отлично работает в C++. Это не гарантированный для работы, но на практике работает. Между ними есть разница.

coryan 03.12.2008 02:55

@coryan, @Martin York: Не только точки последовательности, но и такие вещи, как переупорядочение инструкций процессора, спекулятивное выполнение и недействительность строки кэша на нескольких процессорах. Используйте API потока, это единственный способ быть уверенным.

Zan Lynx 03.12.2008 04:41

Изящный трюк, чтобы продлить срок службы. Обратите внимание, что если бы мы пошли DI, у нас было бы: B(A&), что аккуратно решило бы проблему. Конечно, невозможность создания по умолчанию имеет свои проблемы, поэтому, возможно, B(): mInstanceOfA(A::getInstance()) {}

Matthieu M. 01.04.2010 18:40

@coryan: вы забыли добавить «.. пока не сломается».

peterchen 18.08.2010 20:05

Это просто переносит проблему на деструкторы. Был там, сделал это, получил короткое замыкание и сбой, когда один объект был уничтожен слишком рано при выходе из программы.

jww 08.12.2015 00:21

@jww: Тогда тебе стоит почитать. Потому что мы гарантируем порядок строительства и разрушения, поэтому нет никаких проблем. Вот в чем суть.

Martin York 08.12.2015 01:26

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

jww 08.12.2015 01:41

@jww: Значит, ты делаешь это неправильно. Записано, пока вы не поймете, как работает C++ :-) Окей, прости за задницу. Это легко сделать неправильно. Но я обнаружил, что эту проблему легко исправить благодаря гарантиям языка. Конечно, связки еще можно достать. Но самое простое решение, очевидно, - не использовать глобальные переменные (или статические переменные продолжительности хранения).

Martin York 08.12.2015 02:32

@Loki - спасибо. Думаю, я довольно хорошо это понимаю. Возможно, вы можете обновить свой ответ, чтобы указать порядок уничтожения единиц перевода?

jww 08.12.2015 02:57

@jww. Он уже справляется с этим. Что и было смыслом написания этого ответа. Он обеспечивает соблюдение порядка построения и разрушения для всех статических объектов продолжительности хранения. Хотя для этого вам потребуется немного поработать. Это просто обходной путь для плохой практики программирования, когда у вас есть глобальные объекты, но он гарантирует, что все объекты создаются и уничтожаются в четко определенном порядке. Это означает, что взаимодействия четко определены.

Martin York 08.12.2015 03:14

Простите мое незнание ... Как именно вы это делаете в единицах перевода ?: «Решение, вы должны убедиться, что вы принудительно установили порядок уничтожения ...»

jww 08.12.2015 03:17

Прочтите комментарии в приведенном выше коде. Порядок уничтожения прямо противоположен порядку уничтожения (гарантируется языком). Поэтому все, что вам нужно сделать, это создать их в правильном порядке. Вы можете сделать это, используя хорошо известный шаблон для доступа к объекту в конструкторе.

Martin York 08.12.2015 03:18

«C++ 03 официально не гарантирует, что создание статических функциональных объектов является потокобезопасным». Что ж, он вообще не признает существование многопоточности.

Deduplicator 01.08.2017 20:22

@Deduplicator: и, следовательно, он не дает никаких гарантий, что конструкция является потокобезопасной.

Martin York 01.08.2017 21:11

Да, я знаю это, но, как вы это говорите, похоже, что вы говорите «C++ 03 знает потоки, но по какой-то без сомнения глупой причине они не сделали эту конструкцию поточно-ориентированной» вместо «C + +03 не знает тем, так что вы сами по себе. Используйте ... "

Deduplicator 01.08.2017 21:25

@Loki Стоит отметить, что этот порядок уничтожения не применяется к локальным переменным потока, которые обращаются к обычной статической переменной.

kylefinn 26.10.2017 22:47

Выбор нитей: что касается разрушения, проблема с этим решением заключается в том, что все клиенты A (например, B::instance) должны вызывать A::getInstance() в своем конструкторе, если они хотят использовать A::getInstance() в своем деструкторе. Я согласен с тем, что в большинстве ситуаций это происходит естественным образом, но в некоторых случаях может и не быть. Фактически, я считаю, что именно поэтому механизмы ведения журнала (такие как std::cout) обычно реализуются с использованием идиомы Nifty Counter: это позволяет классу B вести журнал только при уничтожении, что было бы небезопасно, если бы регистратор был реализован, как в этом ответе.

Boris Dalstein 18.03.2018 14:04

См. Также isocpp.org/wiki/faq/ctors#construct-on-first-use-v2: However if a and/or b and/or c fail to use ans in their constructors and/or if any code anywhere gets the address of ans and hands it to some other static object, all bets are off and you have to be very, very careful.

Boris Dalstein 18.03.2018 14:19

We've run into some problems with thestatic initialization order fiasco,and I'm looking for ways to combthrough a whole lot of code to findpossible occurrences. Any suggestionson how to do this efficiently?

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

1) Найдите все глобальные объекты, у которых есть нетривиальные конструкторы, и поместите их в список.

2) Для каждого из этих нетривиально сконструированных объектов сгенерируйте все дерево потенциальных функций, вызываемое их конструкторами.

3) Пройдитесь по дереву функций нетривиального конструктора, и если код ссылается на любые другие нетривиально сконструированные глобальные объекты (которые довольно удобно находятся в списке, который вы создали на первом шаге), у вас есть потенциальный порядок ранней статической инициализации проблема.

4) Повторяйте шаги 2 и 3, пока не исчерпаете список, созданный на шаге 1.

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

Gimpel Software (www.gimpel.com) утверждает, что их инструменты статического анализа PC-Lint / FlexeLint обнаруживают такие проблемы.

У меня был хороший опыт работы с их инструментами, но не с этой конкретной проблемой, поэтому я не могу поручиться за то, насколько они помогут.

Если ваш проект находится в Visual Studio (я пробовал это с VC++ Express 2005 и с Visual Studio 2008 Pro):

  1. Открыть представление класса (Главное меню-> Просмотр-> Просмотр класса)
  2. Разверните каждый проект в своем решении и нажмите «Глобальные функции и переменные».

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

В конце концов, лучший подход - попытаться удалить эти объекты из вашего проекта (иногда проще сказать, чем сделать).

Спасибо, но в моем вопросе вы увидите, что наша платформа - это IBM XL C / C++ под AIX. Visual Studio - это не вариант. Я согласен, что лучше бы от них избавиться; задача найти их всех.

Fred Larson 22.07.2010 08:34

Я просто написал немного кода, чтобы отследить эту проблему. У нас есть кодовая база хорошего размера (более 1000 файлов), которая отлично работала в Windows / VC++ 2005, но вылетала при запуске в Solaris / gcc. Я написал следующий файл .h:

#ifndef FIASCO_H
#define FIASCO_H

/////////////////////////////////////////////////////////////////////////////////////////////////////
// [WS 2010-07-30] Detect the infamous "Static initialization order fiasco"
// email warrenstevens --> [initials]@[firstnamelastname].com 
// read --> http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 if you haven't suffered
// To enable this feature --> define E-N-A-B-L-E-_-F-I-A-S-C-O-_-F-I-N-D-E-R, rebuild, and run
#define ENABLE_FIASCO_FINDER
/////////////////////////////////////////////////////////////////////////////////////////////////////

#ifdef ENABLE_FIASCO_FINDER

#include <iostream>
#include <fstream>

inline bool WriteFiasco(const std::string& fileName)
{
    static int counter = 0;
    ++counter;

    std::ofstream file;
    file.open("FiascoFinder.txt", std::ios::out | std::ios::app);
    file << "Starting to initialize file - number: [" << counter << "] filename: [" << fileName.c_str() << "]" << std::endl;
    file.flush();
    file.close();
    return true;
}

// [WS 2010-07-30] If you get a name collision on the following line, your usage is likely incorrect
#define FIASCO_FINDER static const bool g_psuedoUniqueName = WriteFiasco(__FILE__);

#else // ENABLE_FIASCO_FINDER
// do nothing
#define FIASCO_FINDER

#endif // ENABLE_FIASCO_FINDER

#endif //FIASCO_H

и в файле каждый .cpp в решении я добавил это:

#include "PreCompiledHeader.h" // (which #include's the above file)
FIASCO_FINDER
#include "RegularIncludeOne.h"
#include "RegularIncludeTwo.h"

Когда вы запустите приложение, вы получите такой выходной файл:

Starting to initialize file - number: [1] filename: [p:\\OneFile.cpp]
Starting to initialize file - number: [2] filename: [p:\\SecondFile.cpp]
Starting to initialize file - number: [3] filename: [p:\\ThirdFile.cpp]

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

Заметки:

  • Важно, чтобы вы поместили макрос «FIASCO_FINDER» как можно ближе к началу файла. Если вы поместите его под другим #includes, вы рискуете его сбой, прежде чем идентифицировать файл, в котором вы находитесь.

  • Если вы используете Visual Studio и предварительно скомпилированные заголовки, добавление этой дополнительной строки макроса в все ваших файлов .cpp можно быстро выполнить с помощью диалогового окна «Найти и заменить», чтобы заменить существующий #include "precompiledheader.h" на тот же текст плюс строка FIASCO_FINDER (если вы отметите «регулярные выражения, вы можете использовать« \ n »для вставки многострочного замещающего текста)

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

Chad 24.10.2012 21:44

Чад - часто есть способ произвести быструю замену даже для более чем 1000 файлов (например, если у вас уже есть предварительно скомпилированные заголовки, вы можете выполнить поиск / заменить по этому имени заголовка или написать небольшой скрипт для замены всех файлов ). Я сделал первое в нашем проекте, и в нем было много сотен файлов.

Warren Stevens 30.09.2013 17:33

@Chad С помощью хорошего инструмента grep, такого как grepWin, можно было бы сделать это с помощью регулярных выражений. Замените [\s\S]* (весь файл) на #include "HeaderContainingTheMacro.h"\nFIASCO_FINDER\n$0 для всех ваших файлов * .cpp / cxx / cc. $0 должен вернуть весь файл после размещения вашей преамбулы.

Antonio 27.05.2017 12:10

Есть код, который, по сути, «инициализирует» C++, генерируемый компилятором. Простой способ найти этот код / ​​стек вызовов в то время - создать статический объект с чем-то, что разыменовывает NULL в конструкторе - отключите отладчик и немного исследуйте. Компилятор MSVC создает таблицу указателей функций, которая повторяется для статической инициализации. У вас должна быть возможность получить доступ к этой таблице и определить все статические инициализации, происходящие в вашей программе.

Другие ответы верны, я просто хотел добавить, что получатель объекта должен быть реализован в файле .cpp и не должен быть статическим. Если вы реализуете его в файле заголовка, объект будет создан в каждой библиотеке / фреймворке, из которого вы его вызываете ....

Разве правило одного определения не предотвращает этого?

Ryan Pavlik 14.09.2011 23:30

Некоторые из этих ответов уже устарели. Ради людей из поисковых систем, таких как я:

В Linux и других системах найти экземпляры этой проблемы можно с помощью Google AddressSanitizer.

AddressSanitizer is a part of LLVM starting with version 3.1 and a part of GCC starting with version 4.8

Затем вы должны сделать что-то вроде следующего:

$ g++ -fsanitize=address -g staticA.C staticB.C staticC.C -o static 
$ ASAN_OPTIONS=check_initialization_order=true:strict_init_order=true ./static 
=================================================================
==32208==ERROR: AddressSanitizer: initialization-order-fiasco on address ... at ...
    #0 0x400f96 in firstClass::getValue() staticC.C:13
    #1 0x400de1 in secondClass::secondClass() staticB.C:7
    ...

Подробнее см. Здесь: https://github.com/google/sanitizers/wiki/AddressSanitizerInitializationOrderFiasco

Другие вопросы по теме