Поиск статических задач инициализации С++

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

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

Ответ 1

Решение последовательности инициализации:

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

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

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

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

С++ 11 гарантирует, что это потокобезопасно:

§6.7 [stmt.dcl] p4
Если элемент управления входит в объявление одновременно, когда переменная инициализируется, параллельное выполнение должно ждать завершения инициализации.

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

Обратите внимание: не используйте дважды проверенный шаблон блокировки, чтобы попытаться избежать затрат на блокировку. Это не будет работать в С++ 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.
        }
};

Ответ 2

Я только что написал немного кода, чтобы отследить эту проблему. У нас есть хорошая база кода кода (1000+ файлов), которая отлично работает на Windows/VС++ 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 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 файлов можно выполнить быстро, используя Find-and-replace диалоговое окно для замены существующего #include "precompiledheader.h" с тем же текстом плюс строка FIASCO_FINDER (если вы отключите "регулярные выражения, вы можете использовать" \n "для вставки многострочного текста замены)

Ответ 3

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

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

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

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

Ответ 4

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

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

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

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

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

Ответ 5

Существует код, который по существу "инициализирует" С++, который генерируется компилятором. Легкий способ найти этот код/​​стек вызовов в то время - создать статический объект с чем-то, что вызывает NULL в конструкторе - перерыв в отладчике и немного изучить. Компилятор MSVC устанавливает таблицу указателей на функции, которая повторяется для статической инициализации. Вы должны иметь доступ к этой таблице и определить все статические инициализации, происходящие в вашей программе.

Ответ 6

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

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

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

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

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

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

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

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

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

старый...

MyObject myObject

Новый...

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

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

Ответ 10

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

  • Открыть класс (Главное меню- > Вид- > Класс)
  • Разверните каждый проект в своем решении и нажмите "Глобальные функции и переменные"

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

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

Ответ 11

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