C++ Singleton дизайн шаблона

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

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

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

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

26 ответов

Решение

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

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

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

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

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

class Singleton
{
private:
   Singleton();

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

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

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

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

Ответ @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() {}
};

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

singleton<C>()

с помощью

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

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

Я не нашел реализацию 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>

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

Я знаю два известных мне способа правильно создать класс Singleton.

Первый способ:

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

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

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' в первый раз посредством объявления, а не после этого, а затем удаляет статический объект при завершении программы".

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

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

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

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

#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 Это не потокобезопасный. Вы должны обеспечить безопасность потока.

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

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

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

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

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

C++11 Поточно-безопасная реализация:

 #include <iostream>
 #include <thread>


 class Singleton
 {
     private:
         static Singleton * _instance;
         static std::mutex mutex_;

     protected:
         Singleton(const std::string value): value_(value)
         {
         }
         ~Singleton() {}
         std::string value_;

     public:
         /**
          * Singletons should not be cloneable.
          */
         Singleton(Singleton &other) = delete;
         /**
          * Singletons should not be assignable.
          */
         void operator=(const Singleton &) = delete;

         //static Singleton *GetInstance(const std::string& value);
         static Singleton *GetInstance(const std::string& value)
         {
             if (_instance == nullptr)
             {
                 std::lock_guard<std::mutex> lock(mutex_);
                 if (_instance == nullptr)
                 {
                     _instance = new Singleton(value);
                 }
             }
             return _instance;
         }

         std::string value() const{
             return value_;
         }
 };

 /**
  * Static methods should be defined outside the class.
  */
 Singleton* Singleton::_instance = nullptr;
 std::mutex Singleton::mutex_;


 void ThreadFoo(){
     std::this_thread::sleep_for(std::chrono::milliseconds(10));
     Singleton* singleton = Singleton::GetInstance("FOO");
     std::cout << singleton->value() << "\n";
 }

 void ThreadBar(){
     std::this_thread::sleep_for(std::chrono::milliseconds(1000));
     Singleton* singleton = Singleton::GetInstance("BAR");
     std::cout << singleton->value() << "\n";
 }

 int main()
 {
     std::cout <<"If you see the same value, then singleton was reused (yay!\n" <<
                 "If you see different values, then 2 singletons were created (booo!!)\n\n" <<
                 "RESULT:\n";
     std::thread t1(ThreadFoo);
     std::thread t2(ThreadBar);
     t1.join();
     t2.join();
     std::cout << "Complete!" << std::endl;

     return 0;
 }

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

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

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

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

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

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

Вот имитируемый синглтон с использованием CRTP. Он полагается на небольшого помощника для принудительного применения одного объекта в любой момент (максимум). Чтобы принудительно применить один объект к выполнению программы, удалите сброс (который мы считаем полезным для тестов).

А ConcreteSinleton можно реализовать так:

class ConcreteSingleton : public Singleton<ConcreteSingleton>
{
public:
  ConcreteSingleton(const Singleton<ConcreteSingleton>::PrivatePass&)
      : Singleton<StandardPaths>::Singleton{pass}
  {}
  
  // ... concrete interface
  int f() const {return 42;}

};

А затем использовался с

ConcreteSingleton::instance().f();

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

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).

Я хотел бы показать здесь еще один пример синглтона в C++. Имеет смысл использовать шаблонное программирование. Кроме того, имеет смысл унаследовать свой одноэлементный класс от не копируемых и не перемещаемых классов. Вот как это выглядит в коде:

      #include<iostream>
#include<string>

class DoNotCopy
{
protected:
    DoNotCopy(void) = default;
    DoNotCopy(const DoNotCopy&) = delete;
    DoNotCopy& operator=(const DoNotCopy&) = delete;
};

class DoNotMove
{
protected:
    DoNotMove(void) = default;
    DoNotMove(DoNotMove&&) = delete;
    DoNotMove& operator=(DoNotMove&&) = delete;
};

class DoNotCopyMove : public DoNotCopy,
    public DoNotMove
{
protected:
    DoNotCopyMove(void) = default;
};

template<class T>
class Singleton : public DoNotCopyMove
{
public:
    static T& Instance(void)
    {
        static T instance;
        return instance;
    }

protected:
    Singleton(void) = default;
};

class Logger final: public Singleton<Logger>
{
public:
    void log(const std::string& str) { std::cout << str << std::endl; }
};



int main()
{
    Logger::Instance().log("xx");
}

Разделение на классы NotCopyable и NotMovable позволяет определить более конкретный синглтон (иногда вы хотите переместить единственный экземпляр).

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

Вот мой взгляд на то, как делать правильные синглтоны (и другие нетривиальные статические объекты): https://github.com/alex4747-pub/proper_singleton

Резюме:

  1. Используйте статический список инициализации для создания экземпляров синглтонов в нужное время: после входа в main и до включения многопоточности
  2. Добавьте незначительные улучшения, чтобы сделать его удобным для модульного тестирования.

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

      class Singleton {
private:
    int data;
    static Singleton* instance;
    Singleton();
public:
    static Singleton* getInstance();
};
Singleton* Singleton::instance = 0;
Singleton::Singleton()
{
    this->data = 0;
    cout << "constructor called.." << endl;
}

 

Singleton* Singleton::getInstance() {
    if (!instance) {
        instance = new Singleton();
        return instance;
    }
}
int main() {
    Singleton *s = s->getInstance();
    Singleton *s1 =s1->getInstance();
    }

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

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

Ваш код правильный, за исключением того, что вы не объявляли указатель экземпляра вне класса. Заявления внутри класса статических переменных не рассматриваются заявления в 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 вручную. Нам нужен один его объект для всей программы, поэтому в конце выполнения программы он будет автоматически освобожден.

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

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

#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);
#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);

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

Как насчет использования нового размещения, как это:

class singleton
{
    static singleton *s;
    static unsigned char *buffer[sizeof(singleton)/4 *4] //4 byte align
    static singleton* getinstance()
    {
        if (s == null)
        {
            s = new(buffer) singleton;
        }
        return s;
    }
};
Другие вопросы по тегам