Шаблон "одиночка" в C++

У меня есть вопрос о шаблоне "одиночка".

Я видел, что два случая коснулись статического участника в 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;
    }
};

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

46
задан Michael Petrotta 23 March 2010 в 01:19
поделиться

5 ответов

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

Что касается локальной статики, я какое-то время не использовал 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;

В чем проблема? Давайте проверим порядок, в котором вызываются конструкторы и деструкторы.

Сначала этап построения:

  • Выполняется globalA; , вызывается A :: A ()
  • A :: A () вызовы B :: B ()
  • A :: A () вызывает C :: C ()

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

Во-вторых, фаза разрушения:

  • C :: ~ C () вызывается, потому что он был построен последним из 3
  • B :: ~ B () вызывается .. .oups, он пытается получить доступ к экземпляру C !

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

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

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

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 , чтобы сигнализировать об этом.

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

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

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

4. Паттерн моноид

Поскольку мы говорим о Синглтоне , я думаю, что пришло время представить паттерн Моноид . По сути, это можно рассматривать как вырожденный случай паттерна Flyweight или использование Proxy вместо Singleton .

Шаблон Моноид прост: все экземпляры класса имеют общее состояние.

Я воспользуюсь возможностью, чтобы разоблачить реализацию не-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;
}

Какая польза? Он скрывает тот факт, что состояние является общим, он скрывает синглтон .

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

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

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

60
ответ дан 26 November 2019 в 20:34
поделиться

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

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

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

2
ответ дан 26 November 2019 в 20:34
поделиться

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

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

0
ответ дан 26 November 2019 в 20:34
поделиться

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

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

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

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

Обновление

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

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

Да ладно, неважно.

-1
ответ дан 26 November 2019 в 20:34
поделиться

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

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

4
ответ дан 26 November 2019 в 20:34
поделиться
Другие вопросы по тегам:

Похожие вопросы: