2010-03-23 2 views
46

싱글 톤 패턴에 대한 질문이 있습니다.C++의 싱글 톤 패턴

싱글 톤 클래스의 정적 멤버에 관한 두 가지 경우를 보았습니다.

첫째는 객체가이

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

처럼 포인터? 어느 것이 옳은가요?

+14

Alexandrescu의 "Modern C++ Design"에는 싱글 튼을 안전하고 정확하게 만들려고 노력한 전체 장이 포함되어있어 많은 어둠의 모퉁이를 탐험하고 있습니다. 제 의견으로는 "그냥하지 마세요"라고 요약 할 수 있습니다. –

+0

@Mike - 훌륭한 참고 자료이며 전적으로 동의합니다. –

답변

60

아마 Alexandrescu의 책을 읽어야합니다.

로컬 정적에 대해서는 잠시 동안 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; 

무슨 문제 .... 일을 제어 할 수 있습니까? 생성자와 소멸자가 호출되는 순서를 확인해 봅시다.

첫째, 건설 단계 :

  • A globalA; 실행, A::A()가 호출
  • A::A() 전화 B::B()
  • A::A() 전화 C::C()

우리가 B를 초기화하기 때문에 그것은 잘 작동하고 C 인스턴스가 전나무에 있습니다. t 액세스.

둘째, 파괴 단계 :가 호출되는 3

  • B::~B()으로 구성 마지막 이었기 때문에

    • C::~C()는 oups, 그것은 C의 인스턴스에 액세스하려고 ...라고!

    우리는 따라서

    3. 새로운 전략 여기

    아이디어는 간단하다 ... 파괴, 험에서 정의되지 않은 동작이있다.

    S& S::Instance() { if (MInstance == 0) MInstance = new S(); return *MInstance; } 
    

    사실 여부를 확인합니다 : 포인터가 당신이 전화를받을 것이다 작성한 코드의 전에 0로 설정할 수 있도록 글로벌 내장 기능은 다른 전역하기 전에 초기화, 그것은 테스트가 보장 인스턴스가 정확하지 않습니다.

    그러나 말한에서, 여기에 메모리 누수 및 호출되지 없구요 최악의 소멸자가있다. 솔루션이 존재하며 표준화되었습니다. 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));입니다. 즉, 아무것도 인수를 취하지 않고 아무것도 반환하지 않는 함수에 대한 포인터를 받아들입니다.

    둘째, 어떻게 작동합니까? 이전의 사용 사례와 똑같습니다. 초기화 할 때 함수 호출 포인터를 작성하고 호출 할 때마다 스택을 한 번에 하나씩 비 웁니다. 사실, 함수는 Last-In First-Out 방식으로 호출됩니다.

    여기서 어떻게됩니까? 처음 액세스에

    • 건설 (초기화 괜찮), I는 종료 시간

    • 종료 시간을 CleanUp 방법을 등록하십시오 CleanUp 메서드가 호출됩니다. 그것은 객체를 파괴하기 때문에 (소멸자에서 효과적으로 작업 할 수 있습니다) 포인터를 0으로 재설정하여 신호를 보냅니다.

      만약 내가 이미 파괴 개체의 인스턴스에 전화 (A, BC와 예처럼) 어떻게됩니까

    ? 글쎄,이 경우에는 포인터를 0으로 설정 했으므로 임시 싱글 톤을 다시 작성하고 사이클이 새로 시작됩니다. 내가 스택을 털어 놓고 있기 때문에 오래 살지 않을 것이다.

    Alexandrescu는 화산재가 파괴 된 후 필요할 경우 재에서 부활 할 때 Phoenix Singleton이라고 부릅니다.

    또 다른 대안은 정적 플래그를 가지고 정리 중에 destroyed으로 설정하고 사용자가 null 포인터를 반환하는 등의 싱글 톤 인스턴스를 얻지 못했다는 것을 사용자에게 알리는 것입니다. 내가 포인터를 반환 (또는 참조)을 가지고있는 유일한 문제는 거라고이다 더 나은 희망 아무도의 그것에 delete를 호출 할만큼 멍청 : /는 모노 이드 패턴이

    우리가 얘기하고 있기 때문에

    (4) Singleton 나는 그것이 Monoid 패턴을 소개 할 때라고 생각한다. 본질적으로, 이것은 Flyweight 패턴의 퇴화 된 사례 또는 Proxy의 사용을 Singleton 이상으로 볼 수 있습니다.

    Monoid 패턴은 간단합니다. 클래스의 모든 인스턴스는 공통 상태를 공유합니다.

    나는하지 피닉스 구현 : 혜택은 무엇입니까

    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을 숨 깁니다.

    • 혹시이 개 서로 다른 상태를 가질해야하는 경우 (예를 들어 Factory를 호출하여 Singleton 교체)
    • 를 사용하는 모든 코드를 변경하지 않고 당신이 그것을을 것 가능성이 있습니다
    • Nodoby가 당신의 싱글 톤 인스턴스에서 delete으로 전화를 걸면, 실제로 상태를 관리하고 사고를 예방할 수 있습니다. 어쨌든 악의적 인 사용자에 대해서는 많은 것을 할 수 없습니다! 이 올바르게 처리 할 수 ​​파괴 됐어요 후 경우가

    5. 마지막 단어

    (아무것도하지 않고 로그 등 ...)라고 있도록
  • 당신은, 싱글에 대한 액세스를 제어

    완성 된 것처럼 보아도 나는 멀티 쓰레드 문제를 기꺼이 건너 뛴다는 점을 지적하고 싶습니다 ... Alexandrescu의 Modern C++에서 더 많은 것을 배우십시오!

  • +3

    "포인터 (또는 참조)를 반환 할 때 유일한 문제는 아무도 바보가 없어서 전화를 걸 수 없다는 것입니다./ 소멸자를 비공개로 설정해야합니다. 그렇게 할 수있는 길 밖에. –

    +0

    Monoid - 흥미로운 아이디어. 나는 그것을 좋아한다. 원한다면 내부 싱글 톤을 참조 카운트 할 수도 있습니다. – Skeets

    +0

    @Dennis : 맞아, 잊어 버렸어. –

    4

    어느 쪽도 다른 쪽보다 정확하지 않습니다. 나는 일반적으로 싱글 톤의 사용을 피하려고하는 경향이 있지만, 내가 갈 길이라고 생각하는 것에 직면했을 때, 나는이 두 가지를 모두 사용했고 잘 동작했다.

    포인터 옵션이있는 한 장애는 메모리가 누출 될 수 있다는 것입니다. 반면에, 첫 번째 예제는 작업을 끝내기 전에 파괴 될 수 있습니다. 따라서이 작업에 대해 더 적절한 소유자를 파악하지 않으면 임금 전쟁을 할 수 있습니다. 적절한시기에 그것을 창조하고 파괴하십시오.

    +3

    유출 된 메모리 == 올바르지 않습니다. –

    +4

    그래, 그럼? 나는 누출 된 기억이 정확하다고 결코 말하지 않으려 고 노력했다. 싱글 톤은 메모리 누수보다 나쁘다. –

    +0

    @ dash-tom-bang : 좋습니다. 나는 오해했다. 내 downvote를 제거했습니다. –

    0

    첫 번째 예는 싱글 톤에 더 일반적입니다. 두 번째 예제는 주문형으로 작성된다는 점에서 차이가 있습니다.

    그러나 나는 그들이 글로벌 변수 이상이기 때문에 일반적으로 싱글 톤 (singleton)을 사용하는 것을 피하려고합니다.

    +2

    첫 번째 예제는 요청시 만들어집니다. 메소드 통계는 메소드가 처음 호출 될 때까지 작성되지 않습니다. –

    +0

    * 때때로 필요한 전역 변수 * .... 게다가, 그것은 자신의 클래스/네임 스페이스 안에서 멋지고 깔끔합니다. –

    2

    차이점은 두 번째 것은 메모리 누수 (싱글 톤 자체)이고 첫 번째는 메모리 누출입니다. 정적 객체는 관련 메서드가 처음 호출 될 때 초기화되고 (프로그램이 정상적으로 종료되는 한) 프로그램이 종료되기 전에 소멸됩니다. 포인터가있는 버전은 프로그램 종료시 할당 된 포인터를 남기고 Valgrind와 같은 메모리 검사기는 불평 할 것입니다.

    또한 누군가가 수행하는 것을 막으려 고합니다 delete GlobalClass::instance();?

    위의 두 가지 이유로, 정적을 사용하는 버전이 더 일반적인 방법이며 원래 디자인 패턴 책에 규정 된 방법입니다. 싱글 톤 객체가 파괴 될 때 즉

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

    클래스에게 숨겨진 포인터 변수를 해제 초기화 소멸자를 부여하십시오 : "메모리 누수"불만에 대응

    +0

    더 일반적인 것에 대한 인용문이 있습니까? –

    +0

    @ dash-tom-bang : 예, 원래 디자인 패턴 책입니다. –

    +0

    "좀 더 일반적인 방법"내 경험에 의하면 사람들이 주위에 균일 한 비율로 (그리고 더 자주는 아니지만 구현 전략에 관계없이 잘못 사용함) 사용하는 것을 보여주기 때문에 다른 것을 참조 할 수 있다고 생각했습니다. –

    -1

    는 쉽게 수정이 프로그램 종료 시간.

    이 작업을 완료하면 두 양식이 실제로 동일합니다. 유일한 차이점은 숨겨진 개체에 대한 참조를 반환하는 반면 다른 개체는 포인터를 반환한다는 것입니다.

    업데이트

    @BillyONeal가 지적한 바와 같이 뾰족한-에 객체가 결코 삭제 얻을 수 없기 때문에,이 작동하지 않습니다. 아야.

    나는 그것에 대해 생각조차 싫지만, 더러운 일을하려면 atexit()을 사용할 수 있습니다. 쳇.

    오, 음, 신경 쓰지 마세요.

    +1

    아니요, 인스턴스가 포인터이기 때문에 소멸자가 호출되지 않습니다. 소멸자는 포인터가'delete' 일 때까지 호출되지 않습니다. d. –

    +0

    아아, 맞아. 무시. –

    1

    두 번째 방법 - atexit을 사용하여 개체를 비우고 싶지 않은 경우 키퍼 개체 (예 : auto_ptr 또는 자체 작성된 개체)를 항상 사용할 수 있습니다. 이것은 첫 번째 첫 번째 메소드와 마찬가지로 객체를 사용하기 전에 해제 될 수 있습니다.

    정적 개체를 사용하는 경우 기본적으로 이미 해제되어 있는지 확인할 방법이 없습니다.

    포인터를 사용하는 경우 추가로 정적 bool을 추가하여 단일 톤이 이미 파괴되었는지 (Monoid에서와 같이) 나타낼 수 있습니다. 그럼 당신의 코드는 항상 싱글 톤이 파괴되었는지를 검사 할 수 있습니다. 당신이하려는 일에서 실패 할지라도, 최소한 "부분 오류"또는 "액세스 위반"을 얻지는 않을 것이며, 프로그램은 비정상적인 종료를 피할 것입니다.

    1

    나는 Billy에 동의합니다. 두 번째 방법에서는 을 사용하여 힙에서 동적으로 메모리를 할당합니다. 이 메모리는 을 삭제하지 않는 한 항상 남아 있으며 결코 해제되지 않습니다. 따라서 전역 포인터 접근 방식은 메모리 누수를 만듭니다.

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

    더 나은 방법은 싱글 톤 클래스를 만드는 것입니다. 또한 GetInstance() 함수의 인스턴스 가용성 검사를 피할 수 있습니다. 함수 포인터를 사용하여이 작업을 수행 할 수 있습니다.