Универсальная инструкция/шаблон для C++11/14/17, рассчитанная на single-header / header-only стиль синглтонов/сервисов, чтобы не ловить ODR-проблемы и при этом иметь реальный единый объект на программу.
- По умолчанию (без DLL-макросов): обычный
static Service& instance()(Meyers singleton) → один экземпляр на бинарник/модуль (EXE или конкретная DLL). - Если включён режим DLL:
instance()перенаправляется на экспортируемую функцию → можно получить один экземпляр на процесс (все модули берут из одной DLL). - Для DLL неизбежно нужно, чтобы в одном TU (в проекте DLL) была “реализация” (как у stb-библиотек). Это всё равно остаётся “single-header” паттерном.
- Никаких
.cppот пользователя не надо. - ODR-safe.
- Экземпляр единственный внутри текущего модуля (EXE или DLL, где скомпилирован код).
- Нужен один TU в проекте DLL с
#define SERVICE_DLL_IMPLEMENTATIONперед include. - Все клиенты (EXE, плагины) получают объект через импортируемую функцию из DLL.
- Экземпляр единственный на процесс, если все модули обращаются к одной и той же DLL.
#pragma once
#ifndef SERVICE_SINGLETON_HPP_INCLUDED
#define SERVICE_SINGLETON_HPP_INCLUDED
// =======================
// 1) Конфигурационные макросы
// =======================
//
// SERVICE_USE_DLL_SINGLETON
// Если определён: instance() берёт объект через экспортируемую функцию (общий на процесс, если все модули линкованы к DLL).
//
// SERVICE_DLL_EXPORTS
// Определяется при сборке самой DLL (для dllexport). В остальных модулях не определять (будет dllimport).
//
// SERVICE_DLL_IMPLEMENTATION
// Определяется РОВНО в одном TU внутри проекта DLL, чтобы разместить реализацию export-функции.
// (stb-стиль: single-header, но реализация компилируется в одном месте)
//
// Примечание: для Linux/macOS можно не заморачиваться с dllimport/dllexport,
// но экспорт символа всё равно должен быть в одной DSO.
// =======================
// 2) Атрибуты экспорта
// =======================
#if defined(_WIN32) || defined(__CYGWIN__)
# ifdef SERVICE_DLL_EXPORTS
# define SERVICE_API __declspec(dllexport)
# else
# define SERVICE_API __declspec(dllimport)
# endif
#else
# define SERVICE_API
#endif
// =======================
// 3) Класс сервиса
// =======================
class Service {
public:
static Service& instance() noexcept;
// TODO: public API...
// void foo();
private:
Service() noexcept = default;
Service(const Service&) = delete;
Service& operator=(const Service&) = delete;
};
// =======================
// 4) Реализация instance(): два режима
// =======================
#if !defined(SERVICE_USE_DLL_SINGLETON)
// ---- Режим A: обычный header-only (один на модуль) ----
inline Service& Service::instance() noexcept {
// Meyers singleton: C++11+ thread-safe init
static Service s{};
return s;
}
#else
// ---- Режим B: через DLL (один на процесс, если все используют одну DLL) ----
//
// В этом режиме instance() вызывает экспортируемую функцию.
// Саму экспортируемую функцию нужно определить в одном TU DLL
// через SERVICE_DLL_IMPLEMENTATION.
extern "C" SERVICE_API Service& service_instance() noexcept;
inline Service& Service::instance() noexcept {
return service_instance();
}
# ifdef SERVICE_DLL_IMPLEMENTATION
extern "C" SERVICE_API Service& service_instance() noexcept {
// Живёт в DLL: один экземпляр на эту DLL
static Service s{};
return s;
}
# endif
#endif // SERVICE_USE_DLL_SINGLETON
#endif // SERVICE_SINGLETON_HPP_INCLUDEDНичего не определяешь:
#include "service_singleton.hpp"
void f() {
Service::instance(); // один на модуль (обычно один на exe)
}Это работает одинаково в C++11/14/17.
#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#define SERVICE_DLL_IMPLEMENTATION
#include "service_singleton.hpp"#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#include "service_singleton.hpp"#define SERVICE_USE_DLL_SINGLETON
#include "service_singleton.hpp"И дальше везде:
Service::instance(); // общий для всех модулей, подключённых к этой DLLИнструкция для "вечного" сингелтона (leaky singleton): объект создаётся один раз и не уничтожается при завершении программы. Это убирает проблемы порядка разрушения статиков.
- У сервиса есть сложные зависимости, и ты ловишь краши/UB на выходе из программы из-за порядка разрушения глобальных/статических объектов.
- Тебе проще “никогда не вызывать деструктор”, чем поддерживать корректный shutdown.
- Один экземпляр на модуль (EXE или конкретная DLL, где он скомпилирован).
- Потокобезопасная инициализация (C++11+).
- Нет деструктора → нет order-fiasco на teardown.
#pragma once
#ifndef SERVICE_SINGLETON_HPP_INCLUDED
#define SERVICE_SINGLETON_HPP_INCLUDED
// =======================
// Макросы
// =======================
//
// SERVICE_USE_DLL_SINGLETON
// Если определён: instance() берёт объект через экспортируемую функцию (общий на процесс при использовании одной DLL).
//
// SERVICE_DLL_EXPORTS
// Определяется при сборке DLL (dllexport). В остальных модулях не определять (dllimport).
//
// SERVICE_DLL_IMPLEMENTATION
// Определяется ровно в одном TU внутри DLL для размещения реализации export-функции.
#if defined(_WIN32) || defined(__CYGWIN__)
# ifdef SERVICE_DLL_EXPORTS
# define SERVICE_API __declspec(dllexport)
# else
# define SERVICE_API __declspec(dllimport)
# endif
#else
# define SERVICE_API
#endif
class Service {
public:
static Service& instance() noexcept;
private:
Service() noexcept = default;
Service(const Service&) = delete;
Service& operator=(const Service&) = delete;
};
#if !defined(SERVICE_USE_DLL_SINGLETON)
// ---- Режим A: вечный singleton (один на модуль) ----
inline Service& Service::instance() noexcept {
static Service* p = new Service{};
return *p;
}
#else
// ---- Режим B: вечный singleton через DLL (один на процесс при использовании одной DLL) ----
extern "C" SERVICE_API Service& service_instance() noexcept;
inline Service& Service::instance() noexcept {
return service_instance();
}
# ifdef SERVICE_DLL_IMPLEMENTATION
extern "C" SERVICE_API Service& service_instance() noexcept {
static Service* p = new Service{};
return *p;
}
# endif
#endif // SERVICE_USE_DLL_SINGLETON
#endif // SERVICE_SINGLETON_HPP_INCLUDED#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#define SERVICE_DLL_IMPLEMENTATION
#include "service_singleton.hpp"#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#include "service_singleton.hpp"#define SERVICE_USE_DLL_SINGLETON
#include "service_singleton.hpp"- Это сознательная "утечка". В большинстве программ это нормально: ОС всё равно освобождает память при завершении процесса.
- Если внутри
Serviceесть "внешние" ресурсы (файлы/сокеты/потоки), лучше:- либо всё равно иметь явный
shutdown()и вызывать его контролируемо, - либо держать эти ресурсы в объектах с управляемым жизненным циклом, а singleton — только "точка доступа".
- либо всё равно иметь явный
- В DLL-режиме старайся, чтобы всё выделение/освобождение памяти происходило в одном модуле (особенно Windows/CRT).