Готовые методы реализаций

Готовая реализация Singleton в Qt-стиле (потокобезопасный вариант)

1 просмотров

Введение

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() в коде.