2010-03-23 54 views
46

Tôi có câu hỏi về mẫu đơn.Mẫu Singleton trong C++

Tôi thấy hai trường hợp liên quan đến thành viên tĩnh trong lớp singleton.

Đầu tiên nó là một đối tượng, như thế này

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

Một là một con trỏ, như thế này

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

sự khác biệt giữa hai trường hợp là gì? Cái nào đúng?

+14

Nếu bạn thực sự quan tâm, "Thiết kế C++ hiện đại" của Alexandrescu có cả một chương nhằm cố gắng làm cho người độc thân an toàn và chính xác, khám phá nhiều góc tối của ngôn ngữ. Theo tôi, nó có thể được tóm tắt là "chỉ không". –

+0

@Mike - tham khảo tuyệt vời và tôi đồng ý hoàn toàn. –

Trả lời

60

Có lẽ bạn nên đọc sách của Alexandrescu.

Về tĩnh cục bộ, tôi đã không sử dụng Visual Studio trong một thời gian, nhưng khi biên dịch với Visual Studio 2003, có một địa phương tĩnh phân bổ cho mỗi DLL ... nói về một cơn ác mộng của gỡ lỗi, tôi sẽ nhớ rằng một trong một thời gian:/

1. Lifetime của một Singleton

vấn đề chính về độc thân là công tác quản lý suốt đời.

Nếu bạn cố gắng sử dụng đối tượng, bạn cần phải còn sống và đá. Vấn đề do đó xuất phát từ cả khởi tạo và hủy diệt, đó là một vấn đề phổ biến trong C++ với globals.

Việc khởi tạo thường là điều dễ nhất để sửa. Như cả hai phương pháp đề xuất, nó đủ đơn giản để khởi tạo khi sử dụng lần đầu tiên.

Sự hủy diệt có phần tinh tế hơn một chút. các biến toàn cầu bị hủy theo thứ tự ngược lại mà chúng được tạo ra. Vì vậy, trong trường hợp tĩnh địa phương, bạn không thực sự kiểm soát mọi thứ ....

2. Local tĩnh

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; 

vấn đề ở đây là gì? Hãy kiểm tra thứ tự mà trong đó các constructor và destructors được gọi.

Thứ nhất, giai đoạn xây dựng:

  • A globalA; được thực thi, A::A() được gọi
  • A::A() cuộc gọi B::B()
  • A::A() cuộc gọi C::C()

Nó hoạt động tốt, bởi vì chúng ta khởi tạo BC phiên bản trên linh sam t truy cập.

Thứ hai, giai đoạn phá hủy:

  • C::~C() được gọi là bởi vì nó là xây dựng 3
  • B::~B() cuối cùng được gọi là ... oups, nó cố gắng để truy cập C 's dụ!

Chúng tôi do đó có hành vi không xác định tại hủy diệt, hum ...

3. Chiến lược mới

Ý tưởng ở đây là đơn giản.toàn cầu được xây dựng-in được khởi tạo trước khi globals khác, vì vậy con trỏ của bạn sẽ được thiết lập để 0 trước khi bất kỳ mã bạn đã viết sẽ được gọi, nó đảm bảo rằng các thử nghiệm:

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

sẽ thực sự kiểm tra hay không phải là trường hợp chính xác.

Tuy nhiên đã có lúc được nói, có một rò rỉ bộ nhớ ở đây và tồi tệ nhất là một destructor mà không bao giờ được gọi. Giải pháp tồn tại và được chuẩn hóa. Đây là cuộc gọi đến hàm atexit.

Chức năng atexit cho phép bạn chỉ định một hành động để thực thi trong khi tắt chương trình. Với điều đó, chúng ta có thể viết một singleton alright:

// 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!!! 

Đầu tiên, hãy tìm hiểu thêm về atexit. Chữ ký là int atexit(void (*function)(void));, tức là nó chấp nhận một con trỏ đến một hàm mà không có gì là đối số và cũng không trả về gì cả.

Thứ hai, cách hoạt động? Vâng, chính xác như trường hợp sử dụng trước đó: lúc khởi tạo nó xây dựng một chồng các con trỏ để hoạt động để gọi và hủy diệt nó làm trống ngăn xếp một mục tại một thời điểm. Vì vậy, trong thực tế, các chức năng được gọi là trong một Last-In First-Out thời trang.

Điều gì xảy ra ở đây?

  • Xây dựng về tiếp cận đầu tiên (khởi là tốt), tôi đăng ký phương pháp CleanUp cho thời gian thoát

  • thời gian Exit: phương pháp CleanUp được gọi. Nó phá hủy đối tượng (do đó chúng ta có thể làm việc hiệu quả trong destructor) và thiết lập lại con trỏ thành 0 để báo hiệu nó.

gì xảy ra nếu (như trong ví dụ với A, BC) Tôi kêu gọi các thể hiện của một đối tượng đã bị phá hủy? Vâng, trong trường hợp này, kể từ khi tôi đặt trở lại con trỏ đến 0 Tôi sẽ xây dựng lại một singleton tạm thời và chu kỳ bắt đầu một lần nữa. Nó sẽ không tồn tại lâu mặc dù kể từ khi tôi depiling stack của tôi.

Alexandrescu gọi nó là Phoenix Singleton khi nó khôi phục từ tro của nó nếu cần sau khi bị phá hủy.

Một giải pháp thay thế khác là có cờ tĩnh và đặt nó thành destroyed trong khi dọn dẹp và cho người dùng biết rằng nó không nhận được bản sao của singleton, ví dụ bằng cách trả về một con trỏ rỗng. Vấn đề duy nhất tôi có với trả về một con trỏ (hoặc tham chiếu) là bạn muốn tốt hơn hy vọng không ai đủ ngu ngốc để gọi delete vào nó:/

4. Pattern monoid

Vì chúng ta đang nói về Singleton Tôi nghĩ đã đến lúc giới thiệu mẫu Monoid. Về bản chất, nó có thể được xem như một trường hợp thoái hóa của mẫu Flyweight hoặc sử dụng Proxy trên Singleton.

Mẫu Monoid rất đơn giản: tất cả các phiên bản của lớp đều có chung trạng thái.

tôi sẽ có cơ hội để tiếp xúc với việc thực hiện không-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; 
} 

lợi ích là gì? Nó ẩn một thực tế là nhà nước được chia sẻ, nó ẩn các Singleton.

  • Nếu bạn đã bao giờ cần phải có 2 trạng thái khác nhau, có thể là bạn sẽ quản lý để làm điều đó mà không thay đổi mỗi dòng mã mà sử dụng nó (thay thế Singleton bởi một cuộc gọi đến một Factory ví dụ)
  • Nodoby sẽ gọi delete trên bản sao của bạn, vì vậy bạn thực sự quản lý nhà nước và ngăn ngừa tai nạn ... bạn không thể làm gì nhiều chống lại người dùng độc hại anyway!
  • Bạn kiểm soát quyền truy cập vào các singleton, vì vậy trong trường hợp nó được gọi là sau khi nó bị phá hủy, bạn có thể xử lý nó một cách chính xác (không làm gì cả, đăng nhập, vv ...)

5. từ cuối

Hoàn thành như điều này có vẻ như, tôi muốn chỉ ra rằng tôi đã vui vẻ lướt qua bất kỳ vấn đề đa luồng ... đọc hiện đại của Alexandrescu C++ để tìm hiểu thêm!

+3

"Vấn đề duy nhất tôi có với việc trả lại một con trỏ (hoặc tham khảo) là bạn nên hy vọng không ai đủ ngu ngốc để gọi xóa trên đó: /" Bạn đã hủy riêng tư của bạn, vì vậy họ phải đi cũng ra khỏi con đường của họ để làm như vậy. –

+0

Monoid - ý tưởng thú vị. Tôi thích nó. Nó cũng sẽ cho phép bạn đếm lại nội bộ singleton nếu bạn muốn để nó chỉ tồn tại nếu nó đang được sử dụng. – Skeets

+0

@ Dennis: đúng, quên mất điều đó. –

4

Không chính xác hơn loại kia. Tôi có xu hướng cố gắng tránh việc sử dụng Singleton nói chung, nhưng khi tôi đã phải đối mặt với suy nghĩ đó là con đường để đi, tôi đã sử dụng cả hai và họ đã làm việc tốt.

Một trục trặc với tùy chọn con trỏ là nó sẽ rò rỉ bộ nhớ. Mặt khác, ví dụ đầu tiên của bạn có thể bị phá hủy trước khi bạn hoàn thành nó, vì vậy bạn sẽ có một trận chiến để trả lương bất kể bạn không chọn tìm ra một chủ sở hữu thích hợp hơn cho điều này, có thể tạo và phá hủy nó vào đúng thời điểm.

+3

Bộ nhớ bị rò rỉ == không chính xác. –

+4

Ok, vậy? Tôi chưa bao giờ cố gắng nói rằng bộ nhớ bị rò rỉ là chính xác. Singletons tệ hơn rò rỉ bộ nhớ, IMO. –

+0

@ dash-tom-bang: OK. Tôi đã hiểu nhầm. Đã xóa bỏ phiếu giảm giá của tôi. –

0

Ví dụ đầu tiên của bạn điển hình hơn cho một singleton. Ví dụ thứ hai của bạn khác biệt ở chỗ nó được tạo theo yêu cầu.

Tuy nhiên, tôi sẽ cố gắng tránh sử dụng các trình đơn nói chung vì chúng không có gì khác ngoài các biến toàn cầu.

+2

Ví dụ đầu tiên cũng được tạo theo yêu cầu. Phương thức thống kê không được tạo cho đến khi phương thức của chúng được gọi lần đầu tiên. –

+0

* không có gì hơn các biến toàn cầu * ... mà đôi khi bạn cần. Bên cạnh đó, nó được gói gọn và gọn gàng bên trong lớp/không gian tên của chính nó. –

2

Sự khác biệt là điểm thứ hai rò rỉ bộ nhớ (bản thân singleton) trong khi người thứ nhất không có. Các đối tượng tĩnh được khởi tạo khi lần đầu tiên phương thức liên kết của chúng được gọi và (miễn là chương trình thoát sạch), chúng sẽ bị phá hủy trước khi thoát khỏi chương trình. Phiên bản với con trỏ sẽ để con trỏ được cấp phát tại chương trình thoát và bộ kiểm tra bộ nhớ như Valgrind sẽ khiếu nại.

Ngoài ra, điều gì ngăn người khác làm delete GlobalClass::instance();?

Vì hai lý do trên, phiên bản sử dụng tĩnh là phương pháp phổ biến hơn và phiên bản được quy định trong sách Mẫu thiết kế ban đầu.

+0

Bạn có trích dẫn cho những gì phổ biến hơn không? –

+0

@ dash-tom-bang: Vâng, cuốn sách Mẫu thiết kế ban đầu. –

+0

"phương pháp phổ biến hơn" tôi nghĩ có thể tham khảo một cái gì đó khác, vì kinh nghiệm của tôi cho thấy mọi người sử dụng cả ở mức giá thậm chí (và thường xuyên hơn không, sử dụng chúng sai bất kể chiến lược thực hiện). –

-1

Để đối phó với các khiếu nại "rò rỉ bộ nhớ", có một sửa chữa dễ dàng:

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

Nói cách khác, cung cấp cho các lớp một destructor rằng de-khởi tạo các biến con trỏ ẩn khi đối tượng singleton được destructed tại thời điểm chấm dứt chương trình.

Khi bạn đã thực hiện việc này, hai biểu mẫu thực tế giống nhau. Sự khác biệt đáng kể duy nhất là một trong những trả về một tham chiếu đến một đối tượng ẩn trong khi khác trả về một con trỏ đến nó.

Cập nhật

Như @BillyONeal chỉ ra, điều này sẽ không làm việc vì nhọn-to đối tượng không bao giờ được xóa. Ouch.

Tôi thậm chí ghét nghĩ về điều đó, nhưng bạn có thể sử dụng atexit() để thực hiện công việc dơ bẩn. Sheesh.

Ồ, tốt, đừng bận tâm.

+1

Không, trình hủy sẽ không được gọi vì cá thể là một con trỏ. Hàm hủy không được gọi cho đến khi con trỏ đó là 'xóa' d. –

+0

Aaagh, bạn nói đúng. Xem thường. –

1

Sử dụng cách tiếp cận thứ hai - nếu bạn không muốn sử dụng ngoại lệ để giải phóng đối tượng của mình, thì bạn luôn có thể sử dụng đối tượng thủ môn (ví dụ: auto_ptr, hoặc thứ tự viết). Điều này có thể gây ra giải phóng trước khi bạn làm xong với đối tượng, giống như với phương pháp đầu tiên đầu tiên.

Sự khác biệt là nếu bạn sử dụng đối tượng tĩnh, về cơ bản bạn không có cách nào để kiểm tra xem nó đã được giải phóng hay chưa.

Nếu bạn sử dụng con trỏ, bạn có thể thêm bool tĩnh bổ sung để cho biết nếu singleton đã bị hủy (như trong Monoid). Sau đó, mã của bạn luôn có thể kiểm tra xem singleton đã bị phá hủy chưa, và mặc dù bạn có thể thất bại với những gì bạn định làm, ít nhất bạn sẽ không gặp lỗi "lỗi segmentaion" hoặc "vi phạm truy cập" và chương trình sẽ tránh kết thúc bất thường.

1

Tôi đồng ý với Billy. Trong phương pháp tiếp cận thứ hai, chúng tôi đang phân bổ động bộ nhớ từ heap bằng cách sử dụng mới. Bộ nhớ này luôn luôn và không bao giờ được giải phóng, trừ khi một cuộc gọi đến xóa được thực hiện. Do đó cách tiếp cận con trỏ toàn cầu tạo ra một rò rỉ bộ nhớ.

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

Cách tiếp cận tốt hơn là tạo một lớp đơn. Điều này cũng tránh được việc kiểm tra tính khả dụng của cá thể trong hàm GetInstance(). Điều này có thể đạt được bằng cách sử dụng một con trỏ hàm.

class TSingleton; 

typedef TSingleton* (*FuncPtr) (void); 

class TSingleton { 

TSingleton(); //prevent public object creation 
TSingleton (const TSingleton& pObject); // prevent copying object 
static TSingleton* vObject; // single object of a class 

static TSingleton* CreateInstance (void); 
static TSingleton* Instance  (void); 
public: 

static FuncPtr GetInstance; 
}; 


FuncPtr TSingleton::GetInstance = CreateInstance; 
TSingleton* TSingleton::vObject; 

TSingleton::TSingleton() 
{ 
} 

TSingleton::TSingleton(const TSingleton& pObject) 
{ 
} 

TSingleton* TSingleton::CreateInstance(void) 
{ 
if(vObject == NULL){ 

    // Introduce here some code for taking lock for thread safe creation 
    //... 
    //... 
    //... 

    if(vObject == NULL){ 

     vObject = new TSingleton(); 
     GetInstance = Instance; 
    } 
} 

return vObject; 
} 

TSingleton* TSingleton::Instance(void) 
{ 

return vObject; 

} 

void main() 
{ 

TSingleton::GetInstance(); // this will call TSingleton::Createinstance() 

TSingleton::GetInstance(); // this will call TSingleton::Instance() 

// all further calls to TSingleton::GetInstance will call TSingleton::Instance() which simply returns already created object. 

} 
Các vấn đề liên quan