Введение
Singleton — один из самых обсуждаемых паттернов в программировании: его легко реализовать неправильно, и он часто подвергается критике за создание скрытых глобальных зависимостей. Тем не менее, для определённых задач — единый логгер, менеджер конфигурации, доступ к ресурсу, который действительно должен существовать в приложении ровно один раз, — singleton остаётся практичным решением. В статье разберём потокобезопасную реализацию singleton в стиле, естественном для Qt-проектов.
Концепция
Современный C++ (начиная с C++11) гарантирует потокобезопасность инициализации статической локальной переменной внутри функции — это устраняет необходимость в сложных решениях типа double-checked locking, которые были нужны в более старых стандартах. Singleton на основе статической локальной переменной инициализируется лениво (при первом вызове) и ровно один раз, даже при одновременном обращении из нескольких потоков.
Для Qt-стиля singleton обычно наследует QObject, чтобы пользоваться сигналами/слотами, и явно запрещает копирование и перемещение через удалённые конструкторы.
Пример кода
#pragma once
#include <QObject>
#include <QString>
#include <QMutex>
class AppSettings : public QObject
{
Q_OBJECT
public:
static AppSettings &instance()
{
static AppSettings instance; // потокобезопасная инициализация с C++11
return instance;
}
AppSettings(const AppSettings &) = delete;
AppSettings &operator=(const AppSettings &) = delete;
QString theme() const
{
QMutexLocker locker(&m_mutex);
return m_theme;
}
void setTheme(const QString &theme)
{
{
QMutexLocker locker(&m_mutex);
m_theme = theme;
}
emit themeChanged(theme);
}
signals:
void themeChanged(const QString &theme);
private:
explicit AppSettings(QObject *parent = nullptr) : QObject(parent), m_theme("light") {}
~AppSettings() override = default;
mutable QMutex m_mutex;
QString m_theme;
};
// Использование из разных частей приложения
#include "appsettings.h"
#include <QDebug>
void someFunction()
{
AppSettings::instance().setTheme("dark");
qDebug() << "Текущая тема:" << AppSettings::instance().theme();
}
Пояснения к коду
Метод instance() объявляет static AppSettings instance внутри функции — стандарт C++11 гарантирует, что инициализация этой переменной произойдёт ровно один раз, даже если несколько потоков одновременно впервые вызовут instance(). Удалённые конструктор копирования и оператор присваивания предотвращают случайное создание дополнительных копий singleton.
Доступ к внутреннему состоянию (m_theme) защищён QMutex, поскольку сам singleton может вызываться из разных потоков приложения — без этой защиты одновременное чтение и запись m_theme из разных потоков было бы гонкой данных. Сигнал themeChanged испускается после изменения значения, но за пределами блокировки мьютекса — это предотвращает потенциальный дедлок, если слот, подключённый к сигналу, попытается синхронно обратиться к AppSettings::instance() снова.
Подводные камни
- Неопределённый порядок уничтожения нескольких singleton при завершении программы (если они зависят друг от друга в деструкторах) — статические локальные переменные в разных функциях уничтожаются в порядке, обратном инициализации, но если singleton A использует singleton B в своём деструкторе, а B был инициализирован позже A, это может привести к обращению к уже разрушенному объекту.
- Singleton как скрытая глобальная зависимость усложняет юнit-тестирование — код, напрямую вызывающий
AppSettings::instance(), невозможно протестировать с подменённой (mock) реализацией без дополнительных усилий по внедрению зависимостей; для тестируемого кода предпочтительнее явная передача зависимости через конструктор, даже если «под капотом» она реализована как обёртка над singleton. - Испускание сигнала внутри блокировки мьютекса, если слот, подключённый напрямую (Direct connection) к этому сигналу, пытается захватить тот же мьютекс повторно — приводит к дедлоку; в примере это решено испусканием сигнала за пределами
QMutexLocker. - Использование Singleton там, где задача на самом деле требует нескольких экземпляров в будущем (например, при необходимости работы с несколькими профилями настроек) — рефакторинг singleton в обычный класс с явным управлением экземплярами обычно требует пройти по всем местам использования
instance()в коде.