Правильная реализация

 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, нет указателя, нет мьютекса для самого экземпляра
  • Добавьте мьютекс внутри для потокобезопасного доступа к состоянию
  • Предпочитайте внедрение через конструктор синглтонам для всего, что нужно тестировать