C ++ шаблон проектирования Singleton


738

Недавно я столкнулся с реализацией / реализацией шаблона проектирования Singleton для C ++. Это выглядело так (я взял это из примера из реальной жизни):

// a lot of methods are omitted here
class Singleton
{
   public:
       static Singleton* getInstance( );
       ~Singleton( );
   private:
       Singleton( );
       static Singleton* instance;
};

Из этого объявления я могу сделать вывод, что поле экземпляра инициируется в куче. Это означает, что есть выделение памяти. Что для меня совершенно неясно, когда именно память будет освобождена? Или есть ошибка и утечка памяти? Кажется, что есть проблема в реализации.

Мой главный вопрос: как мне правильно это реализовать?



10
В этой статье вы найдете отличное обсуждение того, как реализовать синглтон, а также безопасность потоков в C ++. aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf

107
@sbi - Только ситхи имеют дело с абсолютами. Может ли подавляющее большинство проблем быть решено без синглетонов? Абсолютно. У одиночек возникают проблемы? Да. Тем не менее, я не могу честно сказать, что они плохие , так как дизайн - это вопрос компромиссов и понимания нюансов вашего подхода.
Дерекердманн

11
@derekerdmann: я не говорил, что вам никогда не нужна глобальная переменная (а когда она вам нужна, иногда лучше использовать Singleton ). Я сказал, что их следует использовать как можно меньше. Прославление Singleton как ценной модели проектирования создает впечатление, что его лучше использовать, а не взламывать , что делает код трудным для понимания, сложным для сопровождения и трудным для тестирования. Вот почему я разместил свой комментарий. Ничто из того, что вы сказали, пока не противоречило этому.
sbi

13
@sbi: То, что вы сказали, было «Не используйте их». Не намного разумнее - «их следует использовать как можно меньше», как вы позже изменили - наверняка вы увидите разницу.
JWD

Ответы:


1108

В 2008 году я предоставил реализацию C ++ 98 шаблона проектирования Singleton, который лениво оценивается, гарантированно уничтожается, не является технически безопасным для потоков:
может ли кто-нибудь предоставить мне образец Singleton в c ++?

Вот обновленная реализация C ++ 11 шаблона проектирования Singleton, которая вычисляется лениво, корректно уничтожается и является поточно-ориентированной .

class S
{
    public:
        static S& getInstance()
        {
            static S    instance; // Guaranteed to be destroyed.
                                  // Instantiated on first use.
            return instance;
        }
    private:
        S() {}                    // Constructor? (the {} brackets) are needed here.

        // C++ 03
        // ========
        // Don't forget to declare these two. You want to make sure they
        // are unacceptable otherwise you may accidentally get copies of
        // your singleton appearing.
        S(S const&);              // Don't Implement
        void operator=(S const&); // Don't implement

        // C++ 11
        // =======
        // We can use the better technique of deleting the methods
        // we don't want.
    public:
        S(S const&)               = delete;
        void operator=(S const&)  = delete;

        // Note: Scott Meyers mentions in his Effective Modern
        //       C++ book, that deleted functions should generally
        //       be public as it results in better error messages
        //       due to the compilers behavior to check accessibility
        //       before deleted status
};

См. Эту статью о том, когда использовать синглтон: (не часто)
Синглтон: как его использовать

См. Эту две статьи о порядке инициализации и о том, как справиться:
Порядок инициализации статических переменных
Поиск проблем с порядком статической инициализации C ++

В этой статье описывается время жизни:
каково время жизни статической переменной в функции C ++?

См. Эту статью, в которой обсуждаются некоторые последствия потоков для синглетонов:
экземпляр Singleton, объявленный как статическая переменная метода GetInstance, является ли он потокобезопасным?

См. Эту статью, в которой объясняется, почему блокировка с двойной проверкой не будет работать на C ++:
каковы все распространенные неопределенные поведения, о которых должен знать программист C ++?
Доктор Доббс: C ++ и опасности двойной проверки блокировки: часть I


23
Хороший ответ. Но следует заметить, что это не потокобезопасный stackoverflow.com/questions/1661529/…
Varuna

4
@zourtney: Многие люди не понимают, что вы только что сделали :)
Иоганн Герелл

4
@MaximYegorushkin: Когда это разрушено, очень хорошо определено (нет никакой двусмысленности). См .: stackoverflow.com/questions/246564/…
Мартин Йорк,

3
What irks me most though is the run-time check of the hidden boolean in getInstance()Это предположение о технике реализации. Там не должно быть никаких предположений о том, что он жив. см. stackoverflow.com/a/335746/14065 Вы можете форсировать ситуацию, чтобы она всегда была живой (меньше накладных расходов, чем Schwarz counter). Глобальные переменные имеют больше проблем с порядком инициализации (между единицами компиляции), поскольку вы не форсируете порядок. Преимуществом этой модели является 1) ленивая инициализация. 2) Способность навести порядок (Шварц помогает, но уродливее). Да get_instance()гораздо страшнее.
Мартин Йорк,

3
@kol: Нет. Это не обычный. То, что новички копируют и вставляют код, не задумываясь, не делает его обычным. Вы должны всегда смотреть на вариант использования и убедиться, что оператор присваивания делает то, что ожидается. Копирование и вставка кода приведут вас к ошибкам.
Мартин Йорк,

47

Будучи Синглтоном, вы обычно не хотите, чтобы он был разрушен.

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


4
если удаление никогда не вызывается явно для статического экземпляра Singleton *, разве это технически не будет считаться утечкой памяти?
Эндрю Гаррисон

7
Это не утечка памяти больше, чем простое объявление глобальной переменной.
Илья Н.

15
Чтобы установить что-то прямо ... проблемы утечки памяти в отношении синглетонов совершенно бесполезны. Если у вас есть ресурсы состояния, в которых порядок деконструкции имеет значение, одиночные игры могут быть опасными; но вся память полностью восстанавливается операционной системой после завершения программы ... сводит на нет этот полностью академический момент в 99,9% случаев. Если вы хотите рассуждать о том, что является и не является «утечкой памяти», это нормально, но вы понимаете, что это отвлекает от реальных дизайнерских решений.
jkerian

12
@jkerian: утечки памяти и разрушения в контексте C ++ на самом деле не связаны с утечкой памяти. На самом деле речь идет о контроле над ресурсами. При утечке памяти деструктор не вызывается, и поэтому любые ресурсы, связанные с объектом, освобождаются неправильно. Память - это простой пример, который мы используем при обучении программированию, но есть гораздо более сложные ресурсы.
Мартин Йорк,

7
@ Мартин, я полностью с тобой согласен. Даже если единственным ресурсом является память, вы все равно столкнетесь с проблемами, пытаясь обнаружить РЕАЛЬНЫЕ утечки в вашей программе, если вам придется просмотреть список утечек, отфильтровывая те, которые «не имеют значения». Лучше очистить их все, чтобы любой инструмент, который сообщает об утечках, только сообщал о вещах, которые являются проблемой.
Дельфин

38

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

Я предпочитаю этот вид реализации (на самом деле, я не правильно сказал, что предпочитаю, потому что максимально избегаю синглетонов):

class Singleton
{
private:
   Singleton();

public:
   static Singleton& instance()
   {
      static Singleton INSTANCE;
      return INSTANCE;
   }
};

У него нет динамического выделения памяти.


3
В некоторых случаях эта ленивая инициализация не является идеальным шаблоном для подражания. Один пример - если конструктор синглтона выделяет память из кучи, и вы хотите, чтобы это распределение было предсказуемым, например, во встроенной системе или в другой жестко контролируемой среде. Когда я предпочитаю использовать шаблон Singleton, я предпочитаю создавать экземпляр как статический член класса.
ДМА

3
Для многих более крупных программ, особенно с динамическими библиотеками. Любой глобальный или статический объект, который не является примитивным, может привести к сбоям / сбоям при выходе из программы на многих платформах из-за проблем разрушения при выгрузке библиотек. Это одна из причин, по которой многие соглашения о кодировании (включая Google) запрещают использование нетривиальных статических и глобальных объектов.
obecalp

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

Что мешает пользователю назначить это нескольким объектам, где компилятор за кулисами использует свой собственный конструктор копирования?
Тони Тэннус

19

Ответ @Loki Astari отличный.

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

В этом случае std::shared_ptrможно использовать, чтобы сохранить синглтон для всех пользователей, даже когда в конце программы вызываются статические деструкторы:

class Singleton
{
public:
    Singleton(Singleton const&) = delete;
    Singleton& operator=(Singleton const&) = delete;

    static std::shared_ptr<Singleton> instance()
    {
        static std::shared_ptr<Singleton> s{new Singleton};
        return s;
    }

private:
    Singleton() {}
};

9

Другая нераспределительная альтернатива: создайте синглтон, скажем, класса C, как вам это нужно:

singleton<C>()

с помощью

template <class X>
X& singleton()
{
    static X x;
    return x;
}

Ни этот, ни ответ Кэтэлина не являются автоматически поточно-ориентированными в текущем C ++, но будут в C ++ 0x.


В настоящее время под gcc это потокобезопасно (и было некоторое время).
Мартин Йорк,

13
Проблема с этим дизайном заключается в том, что при использовании нескольких библиотек. У каждой библиотеки есть собственная копия сингтона, который эта библиотека использует. Так что это уже не синглтон.
Мартин Йорк,

6

Я не нашел реализацию CRTP среди ответов, поэтому вот она:

template<typename HeirT>
class Singleton
{
public:
    Singleton() = delete;

    Singleton(const Singleton &) = delete;

    Singleton &operator=(const Singleton &) = delete;

    static HeirT &instance()
    {
        static HeirT instance;
        return instance;
    }
};

Чтобы использовать просто наследовать ваш класс от этого, например: class Test : public Singleton<Test>


1
Не удалось заставить это работать с C ++ 17, пока я не сделал защищенный конструктор по умолчанию и '= default;'.
WFranczyk

6

Решение в принятом ответе имеет существенный недостаток - деструктор для синглтона вызывается после того, как элемент управления покидает main()функцию. На самом деле могут быть проблемы, когда некоторые зависимые объекты размещены внутри main.

Я столкнулся с этой проблемой, когда пытался ввести Singleton в приложении Qt. Я решил, что все мои диалоговые окна настройки должны быть Singletons, и принял шаблон выше. К сожалению, основной класс Qt QApplicationбыл размещен в стеке в mainфункции, и Qt запрещает создавать / уничтожать диалоги, когда объект приложения недоступен.

Вот почему я предпочитаю кучу выделенных синглетонов. Я предоставляю явные init()и term()методы для всех синглетонов и вызываю их внутри main. Таким образом, я полностью контролирую порядок создания / уничтожения синглетонов, а также гарантирую, что синглтоны будут созданы, независимо от того, кто-то звонил getInstance()или нет.


2
Если вы ссылаетесь на принятый в настоящее время ответ, ваше первое утверждение неверно. Деструктор не вызывается до тех пор, пока все статические объекты продолжительности хранения не будут уничтожены.
Мартин Йорк

5

Вот простая реализация.

#include <Windows.h>
#include <iostream>

using namespace std;


class SingletonClass {

public:
    static SingletonClass* getInstance() {

    return (!m_instanceSingleton) ?
        m_instanceSingleton = new SingletonClass : 
        m_instanceSingleton;
    }

private:
    // private constructor and destructor
    SingletonClass() { cout << "SingletonClass instance created!\n"; }
    ~SingletonClass() {}

    // private copy constructor and assignment operator
    SingletonClass(const SingletonClass&);
    SingletonClass& operator=(const SingletonClass&);

    static SingletonClass *m_instanceSingleton;
};

SingletonClass* SingletonClass::m_instanceSingleton = nullptr;



int main(int argc, const char * argv[]) {

    SingletonClass *singleton;
    singleton = singleton->getInstance();
    cout << singleton << endl;

    // Another object gets the reference of the first object!
    SingletonClass *anotherSingleton;
    anotherSingleton = anotherSingleton->getInstance();
    cout << anotherSingleton << endl;

    Sleep(5000);

    return 0;
}

Создан только один объект, и эта ссылка на объект возвращается каждый раз после слов.

SingletonClass instance created!
00915CB8
00915CB8

Здесь 00915CB8 - это место в памяти одноэлементного объекта, одинаковое для продолжительности программы, но (обычно!) Разное при каждом запуске программы.

NB Это не потокобезопасный. Вы должны обеспечить безопасность потока.


5

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

class S
{
    public:
        static S& getInstance()
        {
            if( m_s.get() == 0 )
            {
              m_s.reset( new S() );
            }
            return *m_s;
        }

    private:
        static std::unique_ptr<S> m_s;

        S();
        S(S const&);            // Don't Implement
        void operator=(S const&); // Don't implement
};

std::unique_ptr<S> S::m_s(0);

3
Устаревший в C ++ 11. Вместо этого рекомендуется unique_ptr. cplusplus.com/reference/memory/auto_ptr cplusplus.com/reference/memory/unique_ptr
Эндрю

2
Это не потокобезопасно. Лучше сделать m_sлокальный staticиз getInstance()и инициализировать его сразу же без теста.
Галик

2

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

Типичная реализация (взятая из некоторого кода, который у меня уже есть в emacs):

Singleton * Singleton::getInstance() {
    if (!instance) {
        instance = new Singleton();
    };
    return instance;
};

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

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

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


вы можете сделать вывод, поскольку вы видите, что переменная экземпляра является указателем на экземпляр класса.
Артем Баргер

3
Нет необходимости динамически размещать синглтон. На самом деле это плохая идея, поскольку нет способа автоматически отменить выделение ресурсов, используя вышеуказанный дизайн. Пусть он выпадает из области видимости - это не называет деструкторами и просто ленится.
Мартин Йорк,

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

2

Кто-нибудь упоминал std::call_onceи std::once_flag? Большинство других подходов, включая двойную проверку блокировки, не работают.

Одной из основных проблем в реализации одноэлементного шаблона является безопасная инициализация. Единственный безопасный способ - защитить последовательность инициализации с помощью синхронизирующих барьеров. Но сами эти барьеры должны быть благополучно инициированы. std::once_flagмеханизм гарантированной безопасной инициализации


2

Мы недавно обсуждали эту тему в моем классе EECS. Если вы хотите подробно ознакомиться с примечаниями к лекции, посетите http://umich.edu/~eecs381/lecture/IdiomsDesPattsCreational.pdf.

Есть два способа, как я знаю, чтобы правильно создать класс Singleton.

Первый путь:

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

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

class Singleton {
public:
  static Singleton* get_instance();

  // disable copy/move -- this is a Singleton
  Singleton(const Singleton&) = delete;
  Singleton(Singleton&&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  Singleton& operator=(Singleton&&) = delete;

  friend class Singleton_destroyer;

private:
  Singleton();  // no one else can create one
  ~Singleton(); // prevent accidental deletion

  static Singleton* ptr;
};

// auxiliary static object for destroying the memory of Singleton
class Singleton_destroyer {
public:
  ~Singleton_destroyer { delete Singleton::ptr; }
};

Singleton_destroyer будет создан при запуске программы, и "когда программа завершается, все глобальные / статические объекты уничтожаются кодом завершения работы библиотеки времени исполнения (вставленным компоновщиком), поэтому the_destroyer будет уничтожен; его деструктор удалит Singleton, запустив его деструктор «.

Второй путь

Это называется Meyers Singleton, созданный мастером C ++ Скоттом Мейерсом. Просто определите get_instance () по-другому. Теперь вы также можете избавиться от переменной-члена указателя.

// public member function
static Singleton& Singleton::get_instance()
{
  static Singleton s;
  return s;
}

Это удобно, поскольку возвращаемое значение является ссылкой, и вы можете использовать .синтаксис вместо ->доступа к переменным-членам.

«Компилятор автоматически создает код, который сначала создает 's' через объявление, а не после, а затем удаляет статический объект при завершении программы."

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


1

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

struct Store{
   std::array<Something, 1024> data;
   size_t get(size_t idx){ /* ... */ }
   void incr_ref(size_t idx){ /* ... */}
   void decr_ref(size_t idx){ /* ... */}
};

template<Store* store_p>
struct ItemRef{
   size_t idx;
   auto get(){ return store_p->get(idx); };
   ItemRef() { store_p->incr_ref(idx); };
   ~ItemRef() { store_p->decr_ref(idx); };
};

Store store1_g;
Store store2_g; // we don't restrict the number of global Store instances

Теперь где-то внутри функции (например, main) вы можете сделать:

auto ref1_a = ItemRef<&store1_g>(101);
auto ref2_a = ItemRef<&store2_g>(201); 

Реферы не должны хранить указатель на свои Store потому что эта информация предоставляется во время компиляции. Вам также не нужно беспокоиться о времени Storeжизни, потому что компилятор требует, чтобы он был глобальным. Если действительно есть только один случай, Storeтогда в этом подходе нет накладных расходов; с более чем одним экземпляром компилятор должен быть умным в отношении генерации кода. При необходимости, ItemRefкласс может быть даже friendиз Store(вы можете иметь шаблонных друзей!).

Если Storeсам по себе является шаблонным классом, то ситуация становится более запутанной, но все еще возможно использовать этот метод, возможно, путем реализации вспомогательного класса со следующей сигнатурой:

template <typename Store_t, Store_t* store_p>
struct StoreWrapper{ /* stuff to access store_p, e.g. methods returning 
                       instances of ItemRef<Store_t, store_p>. */ };

Теперь пользователь может создать StoreWrapperтип (и глобальный экземпляр) для каждого глобального Storeэкземпляра и всегда получать доступ к хранилищам через свой экземпляр оболочки (таким образом, забывая о мрачных деталях параметров шаблона, необходимых для использования Store).


0

Речь идет об объекте управления временем жизни. Предположим, у вас есть больше, чем синглтонов в вашем программном обеспечении. И они зависят от синглтона Logger. Предположим, что во время уничтожения приложения другой одноэлементный объект использует Logger для регистрации шагов его уничтожения. Вы должны гарантировать, что Logger должен быть очищен последним. Поэтому, пожалуйста, ознакомьтесь с этой статьей: http://www.cs.wustl.edu/~schmidt/PDF/ObjMan.pdf


0

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

#pragma once

#include <memory>

template<typename T>
class Singleton
{
private:
  static std::weak_ptr<T> _singleton;
public:
  static std::shared_ptr<T> singleton()
  {
    std::shared_ptr<T> singleton = _singleton.lock();
    if (!singleton) 
    {
      singleton.reset(new T());
      _singleton = singleton;
    }

    return singleton;
  }
};

template<typename T>
std::weak_ptr<T> Singleton<T>::_singleton;

0

Ваш код верен, за исключением того, что вы не объявили указатель экземпляра вне класса . Внутренние объявления класса статических переменных не считаются объявлениями в C ++, однако это разрешено в других языках, таких как C # или Java и т. Д.

class Singleton
{
   public:
       static Singleton* getInstance( );
   private:
       Singleton( );
       static Singleton* instance;
};
Singleton* Singleton::instance; //we need to declare outside because static variables are global

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


-1

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


2
К сожалению нет. Это было подробно обсуждено некоторыми из лучших разработчиков C ++. Двойная проверка блокировки сломана в C ++ 03.
Мартин Йорк,

-1
#define INS(c) private:void operator=(c const&){};public:static c& I(){static c _instance;return _instance;}

Пример:

   class CCtrl
    {
    private:
        CCtrl(void);
        virtual ~CCtrl(void);

    public:
        INS(CCtrl);

-1

Простой одноэлементный класс, это должен быть ваш файл класса заголовка

#ifndef SC_SINGLETON_CLASS_H
#define SC_SINGLETON_CLASS_H

class SingletonClass
{
    public:
        static SingletonClass* Instance()
        {
           static SingletonClass* instance = new SingletonClass();
           return instance;
        }

        void Relocate(int X, int Y, int Z);

    private:
        SingletonClass();
        ~SingletonClass();
};

#define sSingletonClass SingletonClass::Instance()

#endif

Получите доступ к вашему синглтону так:

sSingletonClass->Relocate(1, 2, 5);

-3

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.