Skip to content

Instantly share code, notes, and snippets.

@NewYaroslav
Last active December 24, 2025 05:36
Show Gist options
  • Select an option

  • Save NewYaroslav/0505b2c3b9d6a930869c11156f20adf8 to your computer and use it in GitHub Desktop.

Select an option

Save NewYaroslav/0505b2c3b9d6a930869c11156f20adf8 to your computer and use it in GitHub Desktop.
Универсальное правило для header-only / “почти header-only” синглтонов/сервисов

Универсальная инструкция/шаблон для 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” паттерном.

Обычный сингелтон Meyers singleton

Режим A: “обычный header-only” (по умолчанию)

  • Никаких .cpp от пользователя не надо.
  • ODR-safe.
  • Экземпляр единственный внутри текущего модуля (EXE или DLL, где скомпилирован код).

Режим B: “единый экземпляр через DLL”

  • Нужен один TU в проекте DLL с #define SERVICE_DLL_IMPLEMENTATION перед include.
  • Все клиенты (EXE, плагины) получают объект через импортируемую функцию из DLL.
  • Экземпляр единственный на процесс, если все модули обращаются к одной и той же DLL.

Шаблон single-header: service_singleton.hpp

#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

Как использовать

Обычный header-only режим (без DLL)

Ничего не определяешь:

#include "service_singleton.hpp"

void f() {
    Service::instance(); // один на модуль (обычно один на exe)
}

Это работает одинаково в C++11/14/17.

Режим “единый экземпляр через DLL”

В проекте DLL (ровно в одном .cpp, например service_dll.cpp)

#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#define SERVICE_DLL_IMPLEMENTATION
#include "service_singleton.hpp"

В остальных TU этой DLL

#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#include "service_singleton.hpp"

В EXE/плагинах (которые линкуются к DLL)

#define SERVICE_USE_DLL_SINGLETON
#include "service_singleton.hpp"

И дальше везде:

Service::instance(); // общий для всех модулей, подключённых к этой DLL

Вечный сингелтон (C++11/14/17), single-header / header-only

Инструкция для "вечного" сингелтона (leaky singleton): объект создаётся один раз и не уничтожается при завершении программы. Это убирает проблемы порядка разрушения статиков.

Когда использовать

  • У сервиса есть сложные зависимости, и ты ловишь краши/UB на выходе из программы из-за порядка разрушения глобальных/статических объектов.
  • Тебе проще “никогда не вызывать деструктор”, чем поддерживать корректный shutdown.

Что гарантирует

  • Один экземпляр на модуль (EXE или конкретная DLL, где он скомпилирован).
  • Потокобезопасная инициализация (C++11+).
  • Нет деструктора → нет order-fiasco на teardown.

Single-header шаблон

#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

Как подключать (DLL режим)

В проекте DLL — ровно в одном .cpp (например service_dll.cpp)

#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#define SERVICE_DLL_IMPLEMENTATION
#include "service_singleton.hpp"

В остальных TU этой DLL

#define SERVICE_USE_DLL_SINGLETON
#define SERVICE_DLL_EXPORTS
#include "service_singleton.hpp"

В EXE/плагинах (которые линкуются к этой DLL)

#define SERVICE_USE_DLL_SINGLETON
#include "service_singleton.hpp"

Замечания (важные)

  1. Это сознательная "утечка". В большинстве программ это нормально: ОС всё равно освобождает память при завершении процесса.
  2. Если внутри Service есть "внешние" ресурсы (файлы/сокеты/потоки), лучше:
    • либо всё равно иметь явный shutdown() и вызывать его контролируемо,
    • либо держать эти ресурсы в объектах с управляемым жизненным циклом, а singleton — только "точка доступа".
  3. В DLL-режиме старайся, чтобы всё выделение/освобождение памяти происходило в одном модуле (особенно Windows/CRT).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment