Синглтон-паттерн в C ++ - PullRequest
       15

Синглтон-паттерн в C ++

46 голосов
/ 23 марта 2010

У меня вопрос по шаблону синглтона.

Я видел два случая, касающихся статического члена в синглтон-классе.

Сначала это объект, подобный этому

class CMySingleton
{
public:
  static CMySingleton& Instance()
  {
    static CMySingleton singleton;
    return singleton;
  }

// Other non-static member functions
private:
  CMySingleton() {}                                  // Private constructor
  ~CMySingleton() {}
  CMySingleton(const CMySingleton&);                 // Prevent copy-construction
  CMySingleton& operator=(const CMySingleton&);      // Prevent assignment
};

Один указатель, как это

class GlobalClass
{
    int m_value;
    static GlobalClass *s_instance;
    GlobalClass(int v = 0)
    {
        m_value = v;
    }
  public:
    int get_value()
    {
        return m_value;
    }
    void set_value(int v)
    {
        m_value = v;
    }
    static GlobalClass *instance()
    {
        if (!s_instance)
          s_instance = new GlobalClass;
        return s_instance;
    }
};

В чем разница между этими двумя случаями? Какой из них правильный?

Ответы [ 8 ]

60 голосов
/ 23 марта 2010

Вероятно, вам следует прочитать книгу Александреску.

Что касается локальной статики, я некоторое время не использовал Visual Studio, но при компиляции с Visual Studio 2003 для каждой DLL была выделена одна локальная статика... поговорим о кошмаре отладки, я его немного запомню: /

1.Время жизни синглтона

Основная проблема синглетонов - управление временем жизни.

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

Инициализацию обычно проще всего исправить.Как предполагают оба метода, его достаточно просто инициализировать при первом использовании.

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

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() и называется ... ups, он пытается получить доступ к экземпляру C!

Таким образом, мы имеем неопределенное поведение при разрушении, гул ...

3.Новая стратегия

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

S& S::Instance() { if (MInstance == 0) MInstance = new S(); return *MInstance; }

будетна самом деле проверьте, является ли экземпляр правильным.

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

Функция atexit позволяет указать действие, которое будет выполнено во время завершения работы программы.С этим мы можем написать синглтон хорошо:

// in s.hpp
class S
{
public:
  static S& Instance(); // already defined

private:
  static void CleanUp();

  S(); // later, because that's 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));, т.е. она принимает указатель на функцию, которая ничего не принимает в качестве аргумента и ничего не возвращает.

Во-вторых, как это работает?Ну, точно так же, как и в предыдущем случае использования: при инициализации он создает стек указателей для вызова функции и при уничтожении очищает стек по одному элементу за раз.Таким образом, по сути, функции вызываются по принципу «первым пришел - первым обслужен».

Что здесь произойдет?

  • Конструкция при первом доступе (инициализация в порядке), Я регистрирую метод CleanUp для времени выхода

  • Время выхода: вызывается метод CleanUp.Он уничтожает объект (таким образом мы можем эффективно выполнять работу в деструкторе) и сбрасывает указатель на 0, чтобы сигнализировать о нем.

Что произойдет, если (как в примере с * 1095)*, B и C) Я вызываю экземпляр уже уничтоженного объекта?Что ж, в этом случае, так как я установил указатель на 0, я восстановлю временный синглтон, и цикл начнется заново.Хотя он долго не проживет, так как я отправляю свой стек.

Александреску назвал его Phoenix Singleton, поскольку он воскресает из пепла, если он нужен после его уничтожения.

Еще одна альтернативадолжен иметь статический флаг и установить его на destroyed во время очистки, и дать пользователю знать, что он не получил экземпляр синглтона, например, возвращая нулевой указатель.Единственная проблема, с которой я возвращаю указатель (или ссылку), состоит в том, что вам лучше надеяться, что никто не настолько глуп, чтобы вызывать delete: /

4. Моноидный паттерн

Поскольку мы говорим о Singleton, я думаю, пришло время представить шаблон Monoid. По сути, его можно рассматривать как вырожденный случай шаблона Flyweight или использование Proxy over 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.

  • Если вам когда-либо понадобится иметь два разных состояния, возможно, вам удастся сделать это, не меняя каждую строку кода, которая его использовала (например, заменив Singleton вызовом Factory)
  • Nodoby собирается позвонить delete на экземпляр вашего синглтона, чтобы вы действительно управляли состоянием и предотвращали несчастные случаи ... вы все равно ничего не можете сделать против злонамеренных пользователей!
  • Вы контролируете доступ к синглтону, поэтому в случае, если он вызывается после его уничтожения, вы можете правильно с ним обращаться (ничего не делать, регистрировать и т. Д.)

5. Последнее слово

Как бы полно это не выглядело, я хотел бы отметить, что я с радостью рассмотрел любые проблемы с многопоточностью ... прочитайте Modern C ++ Александреску, чтобы узнать больше!

4 голосов
/ 23 марта 2010

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

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

2 голосов
/ 23 марта 2010

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

Кроме того, что мешает кому-либо делать delete GlobalClass::instance();?

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

1 голос
/ 05 марта 2012

Я согласен с Билли. Во втором подходе мы динамически выделяем память из кучи, используя new . Эта память остается всегда и никогда не освобождается, если только не был сделан вызов delete . Следовательно, глобальный указатель создает утечку памяти.

class singleton
{
    private:
        static singleton* single;
        singleton()
        {  }
        singleton(const singleton& obj)
        {  }

    public:
        static singleton* getInstance();
        ~singleton()
        {
            if(single != NULL)
            {
                single = NULL;
            }
        }
};

singleton* singleton :: single=NULL;
singleton* singleton :: getInstance()
{
    if(single == NULL)
    {
        single = new singleton;
    }
    return single;
}

int main() {
    singleton *ptrobj = singleton::getInstance();
    delete ptrobj;

    singleton::getInstance();
    delete singleton::getInstance();
    return 0;
}
1 голос
/ 12 июня 2011

Используйте второй подход - если вы не хотите использовать atexit для освобождения вашего объекта, тогда вы всегда можете использовать объект-хранитель (например, auto_ptr или что-то написанное самостоятельно). Это может вызвать освобождение, прежде чем вы закончите с объектом, так же, как с первым первым методом.

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

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

0 голосов
/ 03 октября 2012

Лучше всего создать класс синглтона. Это также позволяет избежать проверки доступности экземпляра в функции GetInstance (). Это может быть достигнуто с помощью указателя на функцию.

class TSingleton;

typedef TSingleton* (*FuncPtr) (void);

class TSingleton {

TSingleton(); //prevent public object creation
TSingleton  (const TSingleton& pObject); // prevent copying object
static TSingleton* vObject; // single object of a class

static TSingleton* CreateInstance   (void);
static TSingleton* Instance     (void);
public:

static FuncPtr  GetInstance; 
};


FuncPtr TSingleton::GetInstance = CreateInstance;
TSingleton* TSingleton::vObject;

TSingleton::TSingleton()
{
}

TSingleton::TSingleton(const TSingleton& pObject)
{
}

TSingleton* TSingleton::CreateInstance(void)
{
if(vObject == NULL){

    // Introduce here some code for taking lock for thread safe creation
    //...
    //...
    //...

    if(vObject == NULL){

        vObject = new TSingleton();
        GetInstance = Instance;
    }
}

return vObject;
}

TSingleton* TSingleton::Instance(void)
{

return vObject;

}

void main()
{

TSingleton::GetInstance(); // this will call TSingleton::Createinstance()

TSingleton::GetInstance(); // this will call TSingleton::Instance()

// all further calls to TSingleton::GetInstance will call TSingleton::Instance() which simply returns already created object. 

}
0 голосов
/ 23 марта 2010

В ответ на жалобы на «утечку памяти» есть простое решение:

// dtor
~GlobalClass()
{
    if (this == s_instance)
        s_instance = NULL;
}

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

Как только вы это сделаете, две формы практически идентичны.Единственное существенное отличие состоит в том, что один возвращает ссылки на скрытый объект, а другой возвращает указатель на него.

Обновление

Как указывает @BillyONeal, этоне работает, потому что указанный объект никогда не получает удаленный .Ой.

Я ненавижу даже думать об этом, но вы можете использовать atexit(), чтобы сделать грязную работу.Шиш.

О, ну, неважно.

0 голосов
/ 23 марта 2010

Ваш первый пример более типичен для синглтона. Ваш второй пример отличается тем, что он создается по требованию.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...