Введение
Singleton — порождающий шаблон проектирования, гарантирующий, что объект существует только в одном экземпляре, и предоставляющий глобальную точку доступа к нему (современные критики считают, что это паттерн реализации, а не проектирования).
Итак, представим, что у нас есть некоторые данные Info, которые можно получить из базы данных. Эти данные используются в разных частях программы и не изменяются во время её выполнения. Похоже, это идеальный кандидат для кэширования с помощью Singleton.
Реализация
Набираем Синглтон Майерса.
Не используйте в современном С++ реализацию, приведенную в "Design Patterns" GoF, она имеет много проблем, в частности, data race в многопоточных программах.
class GlobalInfo final
{
public:
static const GlobalInfo& getInstance()
{
static GlobalInfo instance;
return instance;
}
[[nodiscard]]
const Info& info() const noexcept
{
return info_;
}
private:
GlobalInfo() :
info_(getInfo()) { }
GlobalInfo(const GlobalInfo&) = delete;
GlobalInfo(GlobalInfo&&) noexcept = delete;
GlobalInfo& operator=(const GlobalInfo&) = delete;
GlobalInfo& operator=(GlobalInfo&&) noexcept = delete;
Info info_;
};И используем.
int proccessInfo()
{
const auto& info = GlobalInfo::getInstance().info();
// ...
}Вроде всё хорошо, и это даже работает, но... тут мы решаем покрыть proccessInfo юнит-тестами.
Упс, функция proccessInfo неявно зависит от GlobalInfo, который невозможно инициализировать в тестовой среде.
Существуют разные способы обойти данную проблему, например, принимать info в качестве аргумента. Но давайте не будем менять сигнатуру proccessInfo, а просто добавим еще одну абстракцию.

Скажем, что в GlobalInfo находятся не просто глобальные данные, а глобальные данные по умолчанию. А доступ к текущем глобальным данным будем предоставлять через функции getGlobalInfo и setGlobalInfo. Теперь proccessInfo ничего не знает о GlobalInfo и зависит только от интерфейса getGlobalInfo, что уменьшает связность.
Далее я буду использовать глобальные переменные и свободные функции, что не меняет сути, просто мне так больше нравится ?.
Перепишем код.
// global_info.h
// ...
[[nodiscard]]
const Info& getDefaultInfo();
[[nodiscard]]
const Info& getGlobalInfo();
void setGlobalInfo(const Info* new_info) noexcept;// global_info.cpp
// ...
const Info& getDefaultInfo()
{
static Info instance = getInfo();
return instance;
}
constinit std::atomic<const Info*> global_info;
const Info& getGlobalInfo()
{
const auto* instance = global_info.load(std::memory_order_relaxed);
if(!instance) [[unlikely]]
{
const auto* default_info = std::addressof(getDefaultInfo());
global_info.compare_exchange_weak(instance, default_info,
std::memory_order_relaxed);
return *default_info;
}
return *instance;
}
void setGlobalInfo(const Info* new_info) noexcept
{
global_info.store(new_info, std::memory_order_relaxed);
}И используем его.
int proccessInfo()
{
const auto& info = getGlobalInfo();
// ...
}Теперь функция proccessInfo может быть покрыта юнит-тестами. Для этого достаточно в тестовой среде установить Mock-объект с помощью setGlobalInfo.
Хорошо, но что если кто-то решит установить в setGlobalInfo указатель на объект, который не доживет до конца использования результата getGlobalInfo?
Существует два решения.
Можно просто написать контракт для данной функции в документации. Например, объект, на который указывает new_info при установке значения через setGlobalInfo, должен пережить все вызовы getGlobalInfo, иначе поведение не определено.
Но давайте подумаем, а что на самом деле требуется от new_info? Мы хотим, чтобы это были данные из синглтона. А синглтон это скорее всего функция или объект со статической функцией, который возвращает ссылку на Info. Давайте так и запишем.
Создадим тип InfoHandler, который является указателем на функцию, возвращающую ссылку на Info. В качестве глобального состояния будем хранить указатель на функцию, а не данные.
// global_info.h
// ...
using InfoHandler = const Info& (*)();
[[nodiscard]]
const Info& getDefaultInfo();
[[nodiscard]]
const Info& getGlobalInfo();
void setGlobalInfo(InfoHandler info_handler) noexcept;// global_info.cpp
// ...
const Info& getDefaultInfo()
{
static Info instance = getInfo();
return instance;
}
constinit std::atomic<const Info& (*)()> global_info;
const Info& getGlobalInfo()
{
auto instance = global_info.load(std::memory_order_relaxed);
if(!instance) [[unlikely]]
{
global_info.compare_exchange_weak(instance, getDefaultInfo,
std::memory_order_relaxed);
return getDefaultInfo();
}
return instance();
}
void setGlobalInfo(InfoHandler info_handler) noexcept
{
global_info.store(info_handler, std::memory_order_relaxed);
}Производительность
А что насчет производительности?
Сразу хочу отметить, что в данной статье нет попытки сделать самый быстрый синглтон, в противном случае все бы было в хедерах.
Цель — сделать синглтон, который является оптимальным и по производительности, и по практичности.
Я просто взял Google Benchmark и вызывал info/getGlobalInfo в нескольких потоках и вот что получилось.

Simple singleton — Синглтон Майерса.
Value singleton — синглтон с getGlobalInfo, хранящий указатель на данные.
Function singleton — синглтон с getGlobalInfo, хранящий указатель на функцию.
Некоторые варианты с продлением жизни объекта, установленного с помощью setGlobalInfo, такие как std::atomic<std::shared_ptr>, std::shared_ptr + std::mutex, были отброшены, так как давали замедление в 30-500 раз.
Учитывая, что время работы вызова ~1 ns и велика погрешность, нельзя утверждать, что синглтон с getGlobalInfo, хранящий указатель на данные, быстрее, чем классический синглтон. Но из графика видно, что они имеют примерно одинаковую производи��ельность.
Кроме того, можно сделать некоторые оценки на основе кода. Рассмотрим получение уже инициализированных по умолчанию данных.
При получении данных из классического синглтона имеем: 1 проверка на то, является ли статическая переменная инициализированной; 1 чтение по адресу.
При получении данных из синглтона с дополнительной функцией, хранящей указатель на данные, имеем: 1 проверка на то, является ли атомарная переменная инициализированной; 1 чтение по адресу.
При получении данных из синглтона с дополнительной функцией, хранящей указатель на функцию, имеем: 1 проверки на то, является ли атомарная переменная инициализированной; 1 проверка на то, является ли статическая переменная инициализированной; 1 дополнительный вызов функции по указатели; 1 чтение по адресу.
Таким образом, удалось сохранить тестируемость кода без потери производительности на абстракцию.