Правильная реализация
1class Config {
2public:
3 static Config& instance() {
4 static Config inst; // C++11 §6.7: инициализация потокобезопасна
5 return inst;
6 }
7
8 void set(std::string_view key, std::string value) { /* ... */ }
9 std::string get(std::string_view key) const { /* ... */ }
10
11private:
12 Config() = default; // запрет конструирования
13 Config(const Config&) = delete;
14 Config& operator=(const Config&) = delete;
15};
16
17Config::instance().set("baud", "115200");
Это вся реализация. Никакого мьютекса, никакого std::call_once, никаких атомарных флагов.
Почему это корректно: стандарт C++11 гарантирует, что инициализация статической переменной в блоке выполняется ровно один раз, даже если несколько потоков одновременно достигают объявления. Компилятор вставляет потокобезопасный защитный механизм вокруг первичной инициализации.
Почему старые подходы были неверны
Двойная проверка блокировки — сломана до C++11
1// СЛОМАНО (до C++11) — гонка данных на ptr_
2class Singleton {
3 static Singleton* ptr_;
4 static std::mutex mtx_;
5public:
6 static Singleton* instance() {
7 if (!ptr_) { // проверка без блокировки — гонка данных!
8 std::lock_guard<std::mutex> lk(mtx_);
9 if (!ptr_) {
10 ptr_ = new Singleton; // запись не видна другим потокам без барьера
11 }
12 }
13 return ptr_;
14 }
15};
Без гарантий упорядочивания памяти CPU или компилятор может переставить запись
в ptr_ до завершения конструктора — другой поток видит ненулевой ptr_
и использует не до конца сконструированный объект.
C++11 исправляет это: модель памяти определяет отношения happens-before, делающие
двойную проверку блокировки безопасной при использовании std::atomic с правильным
упорядочиванием. Но подход с function-static проще и корректен по умолчанию — используйте его.
Синглтон, выделенный на куче, с блокировкой
1// Ненужная сложность
2static Singleton* instance() {
3 static std::once_flag flag;
4 static Singleton* ptr = nullptr;
5 std::call_once(flag, [] { ptr = new Singleton; });
6 return ptr;
7}
std::call_once корректен, но тяжелее статической локальной переменной. Используйте
его, когда нужно вызвать фабричную функцию вместо прямого конструирования, или когда
объект должен быть уничтожен до завершения программы в контролируемом порядке.
Потокобезопасный доступ к состоянию
Существование синглтона потокобезопасно. Доступ к его состоянию — нет, для мутаций всё равно нужна синхронизация:
1class Config {
2public:
3 static Config& instance() {
4 static Config inst;
5 return inst;
6 }
7
8 void set(std::string_view key, std::string value) {
9 std::lock_guard<std::mutex> lk(mtx_);
10 data_[std::string(key)] = std::move(value);
11 }
12
13 std::string get(std::string_view key) const {
14 std::lock_guard<std::mutex> lk(mtx_);
15 auto it = data_.find(std::string(key));
16 return it != data_.end() ? it->second : "";
17 }
18
19private:
20 Config() = default;
21 Config(const Config&) = delete;
22 Config& operator=(const Config&) = delete;
23
24 mutable std::mutex mtx_;
25 std::map<std::string, std::string> data_;
26};
Для доступа с преобладанием чтения используйте std::shared_mutex (C++17):
1mutable std::shared_mutex mtx_;
2
3// Несколько читателей одновременно
4std::string get(std::string_view key) const {
5 std::shared_lock<std::shared_mutex> lk(mtx_);
6 // ...
7}
8
9// Эксклюзивная запись
10void set(std::string_view key, std::string value) {
11 std::unique_lock<std::shared_mutex> lk(mtx_);
12 // ...
13}
Порядок уничтожения и проблема статического времени жизни
Function-static синглтоны уничтожаются в порядке, обратном конструированию. Если синглтон A хранит ссылку на синглтон B, и A уничтожается первым, деструктор B может обратиться к уже уничтоженному A — неопределённое поведение.
1// Опасно: Logger использует Config при уничтожении
2class Logger {
3 ~Logger() {
4 Config::instance().set("last_log", lastMsg_); // Config может быть уже уничтожен!
5 }
6};
Способы защиты:
- Избегайте синглтонов, зависящих от других синглтонов
- Используйте явное управление временем жизни (передавайте зависимости через аргументы конструктора)
- Регистрируйте деструктор через
std::atexitиstd::at_quick_exitв правильном порядке
Это главный аргумент против использования синглтонов вообще.
Когда избегать синглтона
Синглтоны — это глобальное изменяемое состояние с конструктором. Они затрудняют тестирование (нельзя подменить экземпляр на mock), создают скрытые зависимости и вводят проблему порядка уничтожения.
Предпочтительно: передавайте объект по ссылке или std::shared_ptr через
цепочку конструкторов. Это внедрение зависимостей — явное, тестируемое и безопасное по времени жизни.
1// Вместо:
2void SensorTask::run() {
3 Logger::instance().log("started");
4}
5
6// Предпочтительно:
7class SensorTask {
8 ILogger& logger_;
9public:
10 SensorTask(ILogger& logger) : logger_(logger) {}
11 void run() { logger_.log("started"); }
12};
Используйте синглтон только для объектов, которые действительно уникальны в системе (обёртки аппаратных периферийных устройств, дескрипторы ОС) и которые не нужно подменять при тестировании.
Замечание для встраиваемых
На Cortex-M и большинстве bare-metal МК нет многопоточности — нет POSIX-потоков,
нет std::thread. Защитный механизм инициализации синглтона из C++11 по-прежнему
генерирует корректный код (компилируется в простую проверку флага), но гарантия
потокобезопасности неактуальна. На bare-metal статические локальные переменные
фактически всегда безопасны для одноядерного использования.
Если вывод компоновщика показывает символы __cxa_guard_acquire / __cxa_guard_release
и вы хотите их убрать (нет RTOS, нет параллелизма вообще), передайте
-fno-threadsafe-statics в GCC/Clang. Это убирает накладные расходы на защиту,
сохраняя однократную инициализацию.
Краткий справочник
1// Корректный потокобезопасный синглтон — C++11 и новее
2class MySingleton {
3public:
4 static MySingleton& instance() {
5 static MySingleton inst;
6 return inst;
7 }
8 // ... публичный интерфейс ...
9private:
10 MySingleton() = default;
11 MySingleton(const MySingleton&) = delete;
12 MySingleton& operator=(const MySingleton&) = delete;
13};
- Нет
new, нет указателя, нет мьютекса для самого экземпляра - Добавьте мьютекс внутри для потокобезопасного доступа к состоянию
- Предпочитайте внедрение через конструктор синглтонам для всего, что нужно тестировать