Ответ 1
Вероятно, вам стоит прочитать книгу Александреску.
Что касается локального статического, я не использовал Visual Studio какое-то время, но при компиляции с Visual Studio 2003 был один локальный статический ресурс, назначенный на DLL... говорить о кошмаре отладки, я буду помнить, что один на некоторое время:/
1. Время жизни синглтона
Основной вопрос о синглонах - управление жизненным циклом.
Если вы когда-либо пытаетесь использовать объект, вам нужно быть живым и пинать. Таким образом, проблема возникает как из инициализации, так и для уничтожения, что является общей проблемой в С++ с глобальными переменными.
Инициализация, как правило, самая легкая вещь для исправления. Как показывают оба метода, он достаточно прост для инициализации при первом использовании.
Разрушение немного более тонкое. глобальные переменные уничтожаются в обратном порядке, в котором они были созданы. Поэтому в локальном статическом случае вы фактически не контролируете вещи.
2. Локальный статический
struct A
{
A() { B::Instance(); C::Instance().call(); }
};
struct B
{
~B() { C::Instance().call(); }
static B& Instance() { static B MI; return MI; }
};
struct C
{
static C& Instance() { static C MI; return MI; }
void call() {}
};
A globalA;
В чем проблема? Пусть проверяется порядок, в котором вызываются конструкторы и деструкторы.
Сначала фаза строительства:
- Выполняется
-
A globalA;
,A::A()
называется -
A::A()
вызываетB::B()
-
A::A()
вызываетC::C()
Он отлично работает, потому что мы инициализируем экземпляры B
и C
при первом доступе.
Во-вторых, фаза разрушения:
-
C::~C()
вызывается потому, что он был последним построенным из 3 -
B::~B()
вызывается... oups, он пытается получить доступ к экземпляруC
!
Таким образом, мы имеем поведение undefined при разрушении, гулом...
3. Новая стратегия
Идея здесь проста. глобальные встроенные элементы инициализируются перед другими глобальными переменными, поэтому ваш указатель будет установлен в 0
, прежде чем какой-либо из написанного вами кода будет вызван, он гарантирует, что тест:
S& S::Instance() { if (MInstance == 0) MInstance = new S(); return *MInstance; }
Будет проверять, действительно ли экземпляр верен.
Однако, как было сказано, здесь происходит утечка памяти и худший деструктор, который никогда не вызван. Решение существует и стандартизировано. Это вызов функции atexit
.
Функция atexit
позволяет указать действие, выполняемое во время выключения программы. При этом мы можем написать singleton:
// in s.hpp
class S
{
public:
static S& Instance(); // already defined
private:
static void CleanUp();
S(); // later, because that where the work takes place
~S() { /* anything ? */ }
// not copyable
S(S const&);
S& operator=(S const&);
static S* MInstance;
};
// in s.cpp
S* S::MInstance = 0;
S::S() { atexit(&CleanUp); }
S::CleanUp() { delete MInstance; MInstance = 0; } // Note the = 0 bit!!!
Во-первых, позвольте узнать больше о atexit
. Подпись int atexit(void (*function)(void));
, то есть она принимает указатель на функцию, которая ничего не принимает в качестве аргумента и ничего не возвращает.
Во-вторых, как это работает? Ну, точно так же, как в предыдущем случае использования: при инициализации он создает стек указателей для вызова функции, а при уничтожении он стекает стек по одному элементу за раз. Таким образом, функции вызываются в режиме Last-In First-Out.
Что здесь происходит?
-
Конструкция при первом доступе (инициализация прекрасна), я регистрирую метод
CleanUp
для времени выхода -
Время выхода: вызывается метод
CleanUp
. Он разрушает объект (таким образом, мы можем эффективно выполнять работу в деструкторе) и reset указатель на0
, чтобы сигнализировать об этом.
Что произойдет, если (например, в примере с A
, B
и C
) я вызываю экземпляр уже уничтоженного объекта? Ну, в этом случае, поскольку я вернул указатель на 0
, я перестрою временный синглтон, и цикл начнется заново. Он не будет долго жить, так как я удаляю свой стек.
Alexandrescu назвал его Phoenix Singleton
, поскольку он воскресает из своего пепла, если он понадобится после его разрушения.
Другой альтернативой является наличие статического флага и установка его на destroyed
во время очистки, и пусть пользователь знает, что он не получил экземпляр синглтона, например, вернув нулевой указатель. Единственная проблема, с которой я возвращаю указатель (или ссылку), состоит в том, что вам лучше надеяться, что никто не будет настолько глуп, чтобы называть delete
на нем:/
4. Шаблон моноида
Поскольку мы говорим о Singleton
, я думаю, что пришло время представить шаблон Monoid
. По сути, это можно рассматривать как вырожденный случай шаблона Flyweight
или использование Proxy
над Singleton
.
Шаблон Monoid
прост: все экземпляры класса имеют общее состояние.
Я воспользуюсь возможностью разоблачить реализацию не-Phoenix:)
class Monoid
{
public:
void foo() { if (State* i = Instance()) i->foo(); }
void bar() { if (State* i = Instance()) i->bar(); }
private:
struct State {};
static State* Instance();
static void CleanUp();
static bool MDestroyed;
static State* MInstance;
};
// .cpp
bool Monoid::MDestroyed = false;
State* Monoid::MInstance = 0;
State* Monoid::Instance()
{
if (!MDestroyed && !MInstance)
{
MInstance = new State();
atexit(&CleanUp);
}
return MInstance;
}
void Monoid::CleanUp()
{
delete MInstance;
MInstance = 0;
MDestroyed = true;
}
Какая польза? Он скрывает тот факт, что состояние разделяется, оно скрывает Singleton
.
- Если вам когда-либо понадобится 2 разных состояния, возможно, вам удастся это сделать, не меняя каждую строку используемого кода (вместо
Singleton
путем вызоваFactory
) - Nodoby собирается позвонить
delete
на ваш экземпляр singleton, так что вы действительно управляете состоянием и предотвращаете несчастные случаи... вы все равно не можете делать против злонамеренных пользователей! - Вы контролируете доступ к синглтону, поэтому в случае его вызова после его уничтожения вы можете правильно его обрабатывать (ничего не делать, регистрировать и т.д.)
5. Последнее слово
Как бы это ни казалось, я хотел бы указать, что я с радостью просмотрел многопоточные проблемы... читать Alexandrescu Modern С++, чтобы узнать больше!