Шаблон Singleton в С++ - программирование
Подтвердить что ты не робот

Шаблон Singleton в С++

У меня вопрос об одноэлементном шаблоне.

Я видел два случая относительно статического члена в одноэлементном классе.

Сначала это объект, как этот

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;
    }
};

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

4b9b3361

Ответ 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 С++, чтобы узнать больше!

Ответ 2

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

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

Ответ 3

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

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

Для вышеупомянутых двух причин версия, использующая статический, является более распространенным методом и тем, который предписан в оригинальной книге Design Patterns.

Ответ 4

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

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

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

Ответ 5

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

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;
}

Ответ 6

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

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

Ответ 7

Лучший подход - создать одноэлементный класс. Это также позволяет избежать проверки доступности экземпляра в функции 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. 

}

Ответ 8

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

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

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

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

Обновление

Как указывает @BillyONeal, это не сработает, потому что объект с указателем не удаляется. Уч.

Мне не нравится даже думать об этом, но вы можете использовать atexit() для выполнения грязной работы. Sheesh.

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