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

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

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

11 ответов

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

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

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
Если элемент управления вводит объявление одновременно во время инициализации переменной, параллельное выполнение должно ожидать завершения инициализации.

Однако 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.
        }
};

Я просто написал немного кода, чтобы отследить эту проблему. У нас есть кодовая база хорошего размера (более 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" был расположен как можно ближе к верхней части файла. Если вы поместите его ниже некоторого другого #include, вы рискуете его аварийно завершить, прежде чем определить файл, в котором вы находитесь.

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

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

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

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ: удалил избыточный статический объект, я не знаю, почему, но я смешал и сопоставил два метода создания статического вместе в моем исходном примере.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

старый...

MyObject myObject

новый...

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

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

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

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

AddressSanitizer является частью LLVM, начиная с версии 3.1, и частью GCC, начиная с версии 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.

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

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

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

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

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

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