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


735

Нещодавно я натрапив на реалізацію / реалізацію схеми дизайну 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%5Frevid.pdf

106
@sbi - Лише сіт торгує абсолютами. Чи можна переважну більшість проблем вирішити без одиночних? Абсолютно. Чи одинакові викликають проблеми самостійно? Так. Однак я не можу чесно сказати, що вони погані , оскільки дизайн стосується врахування компромісів та розуміння нюансів вашого підходу.
derekerdmann

11
@derekerdmann: Я не казав, що вам ніколи не потрібна глобальна змінна (і коли вона вам потрібна, Singleton іноді краще). Що я сказав, це те, що їх слід використовувати якомога менше. Прославлення Singleton як цінного дизайнерського шаблону створює враження, що це добре використовувати, а не що це хак , робить код важким для розуміння, важким для обслуговування та важким для перевірки. Ось чому я опублікував свій коментар. Жодне з сказаного до цього не суперечило цьому.
sbi

13
@sbi: Ви сказали: "Не використовуйте їх". Не набагато розумніше: "їх слід використовувати якомога менше", які ви згодом змінили - напевно ви бачите різницю.
jwd

Відповіді:


1106

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

Дивіться цю статтю про те, коли користуватися одинарним: (не часто)
Singleton: Як слід його використовувати

Дивіться цю статтю про порядок ініціалізації та як впоратися:
Порядок ініціалізації статичних змінних
Знаходження проблем зі статичним порядком ініціалізації C ++

Дивіться цю статтю, що описує життя:
Який термін служби статичної змінної у функції C ++?

Дивіться цю статтю, в якій обговорюються деякі наслідки для ниток для одиночних клавіш :
Екземпляр Singleton, оголошений статичною змінною методу GetInstance, чи безпечний для потоків?

Дивіться цю статтю, в якій пояснюється, чому подвійне перевірене блокування не працюватиме на C ++:
Які загальні невизначені форми поведінки, про які повинен знати програміст C ++?
Д-р Доббс: С ++ та небезпека подвійного перевірки блокування: I частина


23
Хороша відповідь. Але слід зазначити, що це не безпечно для потоків stackoverflow.com/questions/1661529/…
Варуна

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
@Martin Я повністю з вами згоден. Навіть якщо єдиний ресурс - це пам'ять, ви все одно будете мати проблеми, намагаючись знайти РЕАЛЬНІ витоки у вашій програмі, якщо вам доведеться переглядати список витоків, фільтруючи ті, які "не мають значення". Краще все це очистити, щоб будь-який інструмент, який повідомляє про витоки, повідомляв лише про речі, які є проблемою.
Дельфін

38

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

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

class Singleton
{
private:
   Singleton();

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

Він не має динамічного розподілу пам'яті.


3
У деяких випадках ця лінива ініціалізація не є ідеальною схемою, яку слід слідувати. Один із прикладів - якщо конструктор синглтон виділяє пам'ять з купи, і ви хочете, щоб розподіл було передбачуваним, наприклад, у вбудованій системі чи іншому жорстко контрольованому середовищі. Я вважаю за краще, коли шаблон Singleton є найкращим для використання шаблоном, щоб створити екземпляр як статичний член класу.
dma

3
Для багатьох великих програм, особливо тих, що мають динамічні бібліотеки. Будь-який глобальний або статичний об'єкт, який не є примітивним, може призвести до segfaults / збоїв при виході з програми на багатьох платформах через порядок знищення при завантаженні бібліотек. Це одна з причин, що багато конвенцій про кодування (включаючи Google) забороняють використовувати нетривіальні статичні та глобальні об'єкти.
obecalp

Здається, що статичний екземпляр у такій реалізації має внутрішні зв'язки і матиме унікальні та незалежні копії в різному блоці перекладу, що спричинить заплутаність та неправильну поведінку. Але я бачив багато таких реалізацій, я щось пропускаю?
FaceBro

Що заважає користувачеві призначити це декільком об'єктам, коли компілятор за лаштунками використовує власний конструктор копій?
Tony Tannous

19

Відповідь Локі Астарі відмінна.

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

У цьому випадку 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 - це пам'ять об'єкта Singleton Object, однакова протягом тривалості програми, але (як правило!) Різна при кожному запуску програми.

Примітка. Це не є безпечним для ниток. Ви повинні забезпечити безпеку різьби.


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. Замість цього рекомендується унікальний_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.

Перший шлях:

Реалізуйте його аналогічно тому, як у своєму прикладі. Що стосується знищення, "Singletons зазвичай терпить протягом тривалості запуску програми; більшість ОС відновить пам'ять та більшість інших ресурсів, коли програма припиняється, тому є аргумент, щоб не турбуватися з цього приводу".

Однак, це є гарною практикою очищення після завершення програми. Тому ви можете зробити це за допомогою допоміжного статичного класу 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, запустивши його деструктор ".

Другий шлях

Це називається Майєрс Сінглтон, створений майстром C ++ Скоттом Мейерсом. Просто по-різному визначте get_instance (). Тепер ви також можете позбутися змінної члена вказівника.

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

Це акуратно, оскільки повернене значення є посиланням, і ви можете використовувати .синтаксис замість ->доступу до змінних членів.

"Компілятор автоматично створює код, який створює 's' перший раз через декларацію, а не після цього, а потім видаляє статичний об'єкт при завершенні програми."

Зауважте також, що з Меєром Сінглтон ви "можете потрапити в дуже складну ситуацію, якщо об'єкти покладаються один на одного в момент припинення - коли Синглтон зникає відносно інших об'єктів? Але для простих додатків це працює чудово".


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 singleton. Припустимо, під час знищення програми інший одиночний об'єкт використовує 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.