Sử dụng Singleton đúng trong C++
Nguồn gốc
Singleton là một mẫu thiết kế (design pattern) được đề xuất bởi Gang of Four (bốn đồng tác gỉa) qua cuốn sách nổi tiếng Design Patterns: Elements of Reusable Object-Oriented Software xuất bản năm 1994. Singleton là mẫu thiết kế thuộc nhóm Creational Pattern, tức là liên quan tới việc khởi tạo object
Các tác giả giới giới thiệu singleton như sau
Ensure a class has one instance, and provide a global point of access to it.
Có 2 điểm cần lưu ý ở đây:
-
Khi class chỉ cần sử dụng 1 và duy nhất 1 instance
-
Cung cấp một điểm truy cập toàn cục tới instance này thay vì qua constructor
Vậy cùng phân tích 2 điểm này ở các hướng tiếp cận cài đặt singleton sau trong C++
Eager initialization
class Singleton {
public:
Singleton(const Singleton&) = delete; // 1c. delete copy constructor
static Singleton& getInstance() { // 2a. public function for client code usage
std::cout << "You want my instance, i'm yours" << std::endl;
return _instance;
}
void doOneThing() {
std::cout << "Do thing #1" << std::endl;
}
private:
Singleton() = default; // 1a. Don't public constructor function
static Singleton _instance; // 1b. static private instance
};
Singleton Singleton::_instance; // define static global variable
int main()
{
std::cout << "Hello singleton" << std::endl;
// Singleton s; // failed due to private constructor
// Singleton s1 = Singleton::getInstance(); // failed due to deleted copy constructor
Singleton::getInstance().doOneThing(); // use directly
auto& s = Singleton::getInstance();
s.doOneThing(); // use via reference variable
std::cout << "Good bye!" << std::endl;
return 0;
}
Cách cài đặt đầu tiên này sử dụng 1 biến static member variable _instance, khởi tạo biến này ở global scope. API public getInstance() đơn giản trả ra instance đã được khởi tạo đó, và API này là duy nhất để có thể truy xuất được instance. Lưu ý ở đây constructor được đưa vào private scope, copy constructor thì được delete, như vậy client user không thể khởi tạo hay copy từ instance duy nhất -> đảm bảo điều 1. Tuy nhiên cách cài đặt này yêu cầu chúng ta phải khai báo _instance_ của class ở bên ngoài, khiến code trông không được đẹp, và instance sẽ được khởi tạo ngay khi chương trình chạy.
Lazy initialization
Bây giờ chúng ta sẽ đưa khởi tạo của instance vào bên trong public API như sau
class Singleton {
public:
Singleton(const Singleton&) = delete; // 2b. delete copy constructor
static Singleton& getInstance() { // 2a. public function for client code usage
std::cout << "You want my instance, i'm your" << std::endl;
static Singleton _instance; // 1b. static local variable
return _instance;
}
void doOneThing() {
std::cout << "Do thing #1" << std::endl;
}
private:
Singleton() = default; // 1a. Don't public constructor function
};
Như vậy class Singleton đã trông gọn hơn nhiều, và chỉ khi hàm getInstance() được gọi thì một instance mới được khởi tạo. Lưu ý instance này sẽ chỉ được khởi tạo 1 lần duy nhất, và việc khởi tạo này cũng là thread safe kể từ C++11
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
Note: một số compiler không cài đặt toàn bộ C++11 standard có thể sẽ không hoạt động đúng tỉ dụ như Visual Studio 2013
Classless approach
Cuối cùng là cách cài đặt Singleton mà hoàn toàn không sử dụng tới class
namespace SingletonNS {
namespace {
// custom data goes here
static int meaningOfLife = 42;
}
static void doOneThing() {
std::cout << "Do thing and found " << meaningOfLife << std::endl;
}
}
// Usage
SingletonNS::doOneThing();
Sử dụng namespace chúng ta hoàn toàn có thể có được behavior giống như cài đặt một class Singleton, và việc viết code cũng đơn giản hơn mặc dù code lúc này có vẻ không có cấu trúc rõ ràng như khi đưa vào class.
Kết luận
Singleton là một mẫu thiết kế khá nổi tiếng, mặc dầu vậy nó cũng khá tai tiếng. Nhiều developer coi Singleton như 1 anti-pattern cần tránh. Và thực sự thì điều gì cũng có 2 mặt. Singleton cũng vậy, cũng có nhược điểm như sử dụng global scope (luôn cần cân nhắc và tránh dùng), và có vẻ vi phạm nguyên tắc Single Responsibility Principle. Chính vì vậy, trước khi thực sự cần sử dụng Singleton hay cân nhắc xem mình có thực sự cần nó hay không, nếu thực sự cần thì có thể sử dụng 1 trong các cài đặt như trên (khuyến cáo sử dụng #2).
Tham Khảo
http://gameprogrammingpatterns.com/singleton.html https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/ https://www.youtube.com/watch?v=PPup1yeU45I