From 5b0b20ce18bd5feedcbe20ac7ded19bc042dce8b Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Sun, 23 Nov 2025 23:37:32 +0100 Subject: [PATCH 01/19] feat: add load time singleton --- include/singleton.hpp | 410 +++++++++++++++++++++++++++++------------- 1 file changed, 281 insertions(+), 129 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index ed0f9b0..6b3a0e8 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -1,167 +1,319 @@ #ifndef TESTABLE_SINGLETON_INCLUDED_H #define TESTABLE_SINGLETON_INCLUDED_H +#include #include +#include #include -/// Implements the singleton pattern, but makes unit testing easy. +/// This file implements the singleton pattern, but makes unit testing easy. + +// Types of singletons: +enum class SingletonType : int32_t { + // default case, 99% of the time this is enough + STATIC, + // Some notes: + // Static instances don't always play nice with process termination: + // Problem with atexit handlers: + // - When a static instance is created (e.g. meyers singleton at first use) after + // the atexit handler is registered, then by the time the atexit handler runs, + // this static instance has already been destroyed. + // - This is not a problem if the static instance is created before the atexit handler + // is registered, but when your library is used by some third party or customer processes, + // you cannot rely on this. + // So a singleton type created with this LOAD_TIME parameter provides an alternative: + // - The instance is created at first use as usual. + // - However, its destruction is deferred until "unload time", when your library is unloaded, + // or if linked statically during process termination, but after static destruction phase. + LOAD_TIME +}; + +// fw declare some dependencies +namespace Detail { +template struct SingletonInstance; +} + /** To use the Singleton class normally, inherit from it with the CRTP style, like: - * - * ```cpp - * class MyClass : public Singleton { impl... }; - * ``` - * - * After this, external code is able to get the `MyClass` instance by using `MyClass::Get()`. - * - * To test your singleton, an access-private library implementation is required, such as - * . Using the access-private library, invoke the - * `MyClass::Inject()` function to inject a mock implementation into the singleton. It is also - * possible to reconstruct the Singleton with different constructor arguments by using the - * `MyClass::Reset()` function. - * - * @remark The `Inject()` and `Reset()` functions are NOT thread safe! They should only be used - * in test code sections while implementation code is not running on a different thread. - */ -template -struct Singleton -{ - using BaseType = Singleton; + * + * ```cpp + * class MyClass : public Singleton { impl... }; + * ``` + * + * After this, external code is able to get the `MyClass` instance by using `MyClass::Get()`. + * + * To test your singleton, an access-private library implementation is required, such as + * . Using the access-private library, invoke the + * `MyClass::Inject()` function to inject a mock implementation into the singleton. It is also + * possible to reconstruct the Singleton with different constructor arguments by using the + * `MyClass::Reset()` function. + * + * @remark The `Inject()` and `Reset()` functions are NOT thread safe! They should only be used + * in test code sections while implementation code is not running on a different thread. + */ +template struct Singleton { + using BaseType = Singleton; /// Returns the instance of the class. /** @remark The constructor arguments are only used if the instance is not constructed yet. - */ - template - static T& Get(Args... args) - { - std::call_once(g_onceFlag, [](Args... args) { - g_instance.Emplace(std::forward(args)...); - }, args...); - return *static_cast(g_instance); - } + */ + template static T& Get(Args&&... args); /// Returns the instance of the class without construction. /** @return The instance of the singleton or a `nullptr` if unconstructed. - * @remark This can be useful for singletons that have custom constructor arguments. Code can - * get an already initialied instance, if it has been initialized already, without - * having to specify the arguments. - */ - static T* TryGet() - { - return g_instance; - } + * @remark This can be useful for singletons that have custom constructor arguments. Code can + * get an already initialied instance, if it has been initialized already, without + * having to specify the arguments. + */ + static T* TryGet(); protected: Singleton() noexcept = default; + private: + using Instance = Detail::SingletonInstance; + // just here to catch future regressions + static_assert(type != SingletonType::LOAD_TIME || std::is_trivially_destructible_v, + "Load-time singletons must be trivially destructible"); + Singleton(const Singleton&) = delete; - Singleton& operator =(const Singleton&) = delete; + Singleton& operator=(const Singleton&) = delete; - /// Holds the instance of T, either locally constructed or injected. - static struct Instance final - { - Instance() noexcept = default; - Instance(const Instance&) = delete; - ~Instance() - { - // Destroys the locally-initialized instance. Injected ones are ignored (no ownership). - if (m_pExtern == LOCAL_INSTANCE_ID) - GetBuffer().~T(); - } - Instance& operator =(const Instance&) = delete; - /// Returns the current instance. - operator T* () { return m_pExtern == LOCAL_INSTANCE_ID ? &GetBuffer() : m_pExtern; } - /// Constructs the singleton within the local buffer. - template - void Emplace(Args... args) - { - this->~Instance(); - new (&GetBuffer()) T(std::forward(args)...); - m_pExtern = LOCAL_INSTANCE_ID; - } - /// Sets an external object as the instance. - /** @remark If `ptr` is `nullptr`, this is just reset. - */ - void SetExtern(T* ptr) - { - this->~Instance(); - m_pExtern = ptr; - } - private: - /// A special pointer value to specify that the local buffer is constructed. - static T* const LOCAL_INSTANCE_ID; - /// nullptr if empty; LOCAL_INSTANCE_ID if using internal buffer; - /// pointer to external object otherwise. - T* m_pExtern = nullptr; - /// Returns an (uninitialized) internal buffer for storing T. - static T& GetBuffer() - { - // Static, uninitialized buffer for the singleton's object - static union U { T asT; U(){} ~U(){} } buffer; - return buffer.asT; - } - } g_instance; + static Instance g_instance; /// Holds a flag used for `std::call_once()`. - static struct OnceFlag final - { - OnceFlag() noexcept - { - new (&buffer.asOnceFlag) std::once_flag(); - } + struct OnceFlag final { + OnceFlag() noexcept; OnceFlag(const OnceFlag&) = delete; - ~OnceFlag() - { - buffer.asOnceFlag.~once_flag(); - } - OnceFlag& operator =(const OnceFlag&) = delete; + ~OnceFlag(); + OnceFlag& operator=(const OnceFlag&) = delete; /// Resets the OnceFlag to initial state (allowing another call) - void Reset() - { - this->~OnceFlag(); - new (this) OnceFlag(); - } + void Reset(); /// Returns the internal std::once_flag. May be uninitialized. - operator std::once_flag& () - { - return buffer.asOnceFlag; - } + operator std::once_flag&(); + private: - union U { std::once_flag asOnceFlag; U(){} ~U(){} } buffer; - } g_onceFlag; + union U { + std::once_flag asOnceFlag; + U() { } + ~U() { } + } buffer; + }; + + static OnceFlag g_onceFlag; /// (Re)constructs the internal singleton instance. /** If an existing singleton instance was already constructed, it is destroyed. If an external - * instance was injected, it is overridden with the newly constructed instance. - * - * @remark This function is not thread safe. It is intended for tests, not production code. - */ - template - static T& Reset(Args... args) + * instance was injected, it is overridden with the newly constructed instance. + * + * @remark This function is not thread safe. It is intended for tests, not production code. + */ + template static T& Reset(Args&&... args); + + /// Injects an external instance into the singleton. + /** If an external instance is injected, the `Get()` function + * @param object The object is taken without ownership and must be deleted by the caller. + * @remark If `object` is `nullptr`, it resets the singleton to uninitialized state, and the + * next invocation of `Get()` reconstructs the instance. + */ + static void Inject(T* object); +}; + +// Specialiaze some details of the singleton class in "private" Detail namespace +namespace Detail { + +// Static lifetime singleton specialization (default) +template struct SingletonInstance { + SingletonInstance() noexcept = default; + SingletonInstance(const SingletonInstance&) = delete; + ~SingletonInstance() { - g_onceFlag.Reset(); - return Get(std::forward(args)...); + // Destroys the locally-initialized instance. Injected ones are ignored (no ownership). + if (m_pExtern == LOCAL_INSTANCE_ID) + GetBuffer().~T(); + } + SingletonInstance& operator=(const SingletonInstance&) = delete; + /// Returns the current instance. + operator T*() { return m_pExtern == LOCAL_INSTANCE_ID ? &GetBuffer() : m_pExtern; } + /// Constructs the singleton within the local buffer. + template void Emplace(Args&&... args) + { + this->~SingletonInstance(); + new (&GetBuffer()) T(std::forward(args)...); + m_pExtern = LOCAL_INSTANCE_ID; + } + /// Sets an external object as the instance. + /** @remark If `ptr` is `nullptr`, this is just reset. + */ + void SetExtern(T* ptr) + { + this->~SingletonInstance(); + m_pExtern = ptr; } - /// Injects an external instance into the singleton. - /** If an external instance is injected, the `Get()` function - * @param object The object is taken without ownership and must be deleted by the caller. - * @remark If `object` is `nullptr`, it resets the singleton to uninitialized state, and the - * next invocation of `Get()` reconstructs the instance. - */ - static void Inject(T* object) +private: + /// A special pointer value to specify that the local buffer is constructed. + static T* const LOCAL_INSTANCE_ID; + /// nullptr if empty; LOCAL_INSTANCE_ID if using internal buffer; + /// pointer to external object otherwise. + T* m_pExtern = nullptr; + + /// Returns an (uninitialized) internal buffer for storing T. + static T& GetBuffer() { - if (object) - std::call_once(g_onceFlag, []() {}); - else - g_onceFlag.Reset(); - g_instance.SetExtern(object); + // Static, uninitialized buffer for the singleton's object + static union U { + T asT; + U() { } + ~U() { } + } buffer; + return buffer.asT; } }; + template -typename Singleton::Instance Singleton::g_instance; +T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(0x1); + +// Load time singleton impl: +// Some details: +// The instance is created at first use as usual, but not as a static variable +// but on the heap. Its deleter function is saved to a global registry, in reverse order of creation. +// With the help of GCC's attribute((destructor)) (Clang compatible), the destroy functions +// are called at "unload" time. + +using DeleteLoadTimeSingletonFunc = void (*)(); + +struct LoadTimeSingletonEntry { + DeleteLoadTimeSingletonFunc m_deleteFunc; + LoadTimeSingletonEntry* m_next; +}; +// note to maintainer: all types used by load time singletons must be trivially destructible, +// otherwise they will get destroyed during static destruction phase before "unload" time. +static_assert(std::is_trivially_destructible_v); + +// global stack for load time singletons dtors +inline std::atomic& LoadTimeSingletonEntryStack() +{ + static_assert(std::is_trivially_destructible_v>); + static std::atomic stack { nullptr }; + return stack; +} + +// - Register load time singletons for destruction at "unload" time. +// - The order of destruction is the reverse of the order of registration. +// - The input parameter must point to a static instance. +inline void RegisterLoadTimeSingleton(LoadTimeSingletonEntry* entry) +{ + // - push to head + // from cppreference example: + // make new entry the new head, but if the head + // is no longer what's stored in new entry->m_next + // (some other thread must have inserted am entry just now) + // then put that new head into new entry->m_next and try again + entry->m_next = LoadTimeSingletonEntryStack().load(std::memory_order_relaxed); + while (!LoadTimeSingletonEntryStack().compare_exchange_weak( + entry->m_next, entry, std::memory_order_release, std::memory_order_relaxed)) + ; +} +// attribute destructor to make sure its run after static destruction phase +inline void __attribute__((destructor)) DestroyLoadTimeSingletons() +{ + // memory safety is no concern here + for (auto entry = LoadTimeSingletonEntryStack().load(); entry != nullptr; entry = entry->m_next) { + entry->m_deleteFunc(); + } +} + +// Load-time singleton specialization +// API is compatible with static singleton specialization +// however it has no custom dtor (as that would violate trivial destructibility) +template struct SingletonInstance { + operator T*() { return m_pExtern == LOCAL_INSTANCE_ID ? g_pInternal : m_pExtern; } + template void Emplace(Args&&... args) + { + Reset(); + CreateInternalInstance(); + RegisterLoadTimeSingleton(&g_entry); + m_pExtern = LOCAL_INSTANCE_ID; + } + void SetExtern(T* ptr) + { + Reset(); + m_pExtern = ptr; + } + +private: + // Swapped the dtor on static instance for a Reset function + void Reset() + { + if (m_pExtern == LOCAL_INSTANCE_ID) + DestroyInternalInstance(); + } + // Manage internal instance on heap: + static void CreateInternalInstance() { g_pInternal = new T(); } + static void DestroyInternalInstance() { delete std::exchange(g_pInternal, nullptr); } + + static T* const LOCAL_INSTANCE_ID; + T* m_pExtern = nullptr; + static T* g_pInternal; + static LoadTimeSingletonEntry g_entry; + // we know scalar types are trivially destructible, but add check for intent + static_assert(std::is_trivially_destructible_v); +}; + template -typename Singleton::OnceFlag Singleton::g_onceFlag; +T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(0x1); +template T* SingletonInstance::g_pInternal = nullptr; template -T* const Singleton::Instance::LOCAL_INSTANCE_ID = reinterpret_cast(1); +LoadTimeSingletonEntry SingletonInstance::g_entry + = { SingletonInstance::DestroyInternalInstance, nullptr }; + +} // namespace Detail + +// Implementation of Singleton class + +template template T& Singleton::Get(Args&&... args) +{ + std::call_once( + g_onceFlag, [](Args&&... args) { g_instance.Emplace(std::forward(args)...); }, + std::forward(args)...); + return *static_cast(g_instance); +} +template T* Singleton::TryGet() +{ + return g_instance; +} +template template T& Singleton::Reset(Args&&... args) +{ + g_onceFlag.Reset(); + return Get(std::forward(args)...); +} +template void Singleton::Inject(T* object) +{ + if (object) + std::call_once(g_onceFlag, []() {}); + else + g_onceFlag.Reset(); + g_instance.SetExtern(object); +} +template Singleton::OnceFlag::OnceFlag() noexcept +{ + new (&buffer.asOnceFlag) std::once_flag(); +} +template Singleton::OnceFlag::~OnceFlag() +{ + buffer.asOnceFlag.~once_flag(); +} +template void Singleton::OnceFlag::Reset() +{ + this->~OnceFlag(); + new (this) OnceFlag(); +} +template Singleton::OnceFlag::operator std::once_flag&() +{ + return buffer.asOnceFlag; +} + +template typename Singleton::OnceFlag Singleton::g_onceFlag; +template typename Singleton::Instance Singleton::g_instance; -#endif +#endif // TESTABLE_SINGLETON_INCLUDED_H From 0ed7ee00447c8f6bb4f54e06f6a06bc09eb6dbe1 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 00:05:20 +0100 Subject: [PATCH 02/19] fix: missing ctor params --- include/singleton.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 6b3a0e8..8480709 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -231,7 +231,7 @@ template struct SingletonInstance { template void Emplace(Args&&... args) { Reset(); - CreateInternalInstance(); + CreateInternalInstance(std::forward(args)...); RegisterLoadTimeSingleton(&g_entry); m_pExtern = LOCAL_INSTANCE_ID; } @@ -249,7 +249,10 @@ template struct SingletonInstance { DestroyInternalInstance(); } // Manage internal instance on heap: - static void CreateInternalInstance() { g_pInternal = new T(); } + template static void CreateInternalInstance(Args&&... args) + { + g_pInternal = new T(std::forward(args)...); + } static void DestroyInternalInstance() { delete std::exchange(g_pInternal, nullptr); } static T* const LOCAL_INSTANCE_ID; From d2234343c5a67f8d34ad501826bf55b3739e77d0 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 00:29:24 +0100 Subject: [PATCH 03/19] fix: error on non gcc/clang build and a double free --- include/singleton.hpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 8480709..c2f8d77 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -175,6 +175,7 @@ T* const SingletonInstance::LOCAL_INSTANCE_ID = reinte // Load time singleton impl: // Some details: +// This is platform specific: // The instance is created at first use as usual, but not as a static variable // but on the heap. Its deleter function is saved to a global registry, in reverse order of creation. // With the help of GCC's attribute((destructor)) (Clang compatible), the destroy functions @@ -183,6 +184,7 @@ T* const SingletonInstance::LOCAL_INSTANCE_ID = reinte using DeleteLoadTimeSingletonFunc = void (*)(); struct LoadTimeSingletonEntry { + bool m_isValid = true; // To delete or not (to avoid double free between Reset and __attribute__(destructor)) DeleteLoadTimeSingletonFunc m_deleteFunc; LoadTimeSingletonEntry* m_next; }; @@ -219,7 +221,8 @@ inline void __attribute__((destructor)) DestroyLoadTimeSingletons() { // memory safety is no concern here for (auto entry = LoadTimeSingletonEntryStack().load(); entry != nullptr; entry = entry->m_next) { - entry->m_deleteFunc(); + if (entry->m_isValid) + entry->m_deleteFunc(); } } @@ -228,7 +231,7 @@ inline void __attribute__((destructor)) DestroyLoadTimeSingletons() // however it has no custom dtor (as that would violate trivial destructibility) template struct SingletonInstance { operator T*() { return m_pExtern == LOCAL_INSTANCE_ID ? g_pInternal : m_pExtern; } - template void Emplace(Args&&... args) + template void Emplace(Args... args) { Reset(); CreateInternalInstance(std::forward(args)...); @@ -252,8 +255,13 @@ template struct SingletonInstance { template static void CreateInternalInstance(Args&&... args) { g_pInternal = new T(std::forward(args)...); + g_entry.m_isValid = true; + } + static void DestroyInternalInstance() + { + delete std::exchange(g_pInternal, nullptr); + g_entry.m_isValid = false; } - static void DestroyInternalInstance() { delete std::exchange(g_pInternal, nullptr); } static T* const LOCAL_INSTANCE_ID; T* m_pExtern = nullptr; @@ -261,6 +269,11 @@ template struct SingletonInstance { static LoadTimeSingletonEntry g_entry; // we know scalar types are trivially destructible, but add check for intent static_assert(std::is_trivially_destructible_v); + +// Extra work required to make it platform independent, for now: +#if !defined(__GNUC__) && !defined(__clang__) +#error "LOAD_TIME singletons require GCC or Clang (uses __attribute__((destructor)))" +#endif }; template @@ -268,7 +281,7 @@ T* const SingletonInstance::LOCAL_INSTANCE_ID = rei template T* SingletonInstance::g_pInternal = nullptr; template LoadTimeSingletonEntry SingletonInstance::g_entry - = { SingletonInstance::DestroyInternalInstance, nullptr }; + = { false, SingletonInstance::DestroyInternalInstance, nullptr }; } // namespace Detail From e4b5c93bf790021667fedf36b1f29cd18122f83f Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 00:34:22 +0100 Subject: [PATCH 04/19] fix: by val -> forwarding ref --- include/singleton.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index c2f8d77..d03a9d7 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -231,7 +231,7 @@ inline void __attribute__((destructor)) DestroyLoadTimeSingletons() // however it has no custom dtor (as that would violate trivial destructibility) template struct SingletonInstance { operator T*() { return m_pExtern == LOCAL_INSTANCE_ID ? g_pInternal : m_pExtern; } - template void Emplace(Args... args) + template void Emplace(Args&&... args) { Reset(); CreateInternalInstance(std::forward(args)...); From e1002bbb492f8cc5a6864cc84551c51c31fdb984 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 00:51:27 +0100 Subject: [PATCH 05/19] fix: disable load time singleton on MSVC --- include/singleton.hpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index d03a9d7..21c769e 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -6,12 +6,21 @@ #include #include +// More effort would be required to make load-time singletons work on MSVC +#if defined(__GNUC__) || defined(__clang__) +#define ENABLE_LOAD_TIME_SINGLETON 1 +#else +#define ENABLE_LOAD_TIME_SINGLETON 0 +#endif + /// This file implements the singleton pattern, but makes unit testing easy. // Types of singletons: enum class SingletonType : int32_t { // default case, 99% of the time this is enough - STATIC, + STATIC +#if ENABLE_LOAD_TIME_SINGLETON + , // Some notes: // Static instances don't always play nice with process termination: // Problem with atexit handlers: @@ -26,6 +35,7 @@ enum class SingletonType : int32_t { // - However, its destruction is deferred until "unload time", when your library is unloaded, // or if linked statically during process termination, but after static destruction phase. LOAD_TIME +#endif // ENABLE_LOAD_TIME_SINGLETON }; // fw declare some dependencies @@ -71,9 +81,11 @@ template struct Singlet private: using Instance = Detail::SingletonInstance; +#if ENABLE_LOAD_TIME_SINGLETON // just here to catch future regressions static_assert(type != SingletonType::LOAD_TIME || std::is_trivially_destructible_v, "Load-time singletons must be trivially destructible"); +#endif // ENABLE_LOAD_TIME_SINGLETON Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; @@ -173,6 +185,7 @@ template struct SingletonInstance { template T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(0x1); +#if ENABLE_LOAD_TIME_SINGLETON // Load time singleton impl: // Some details: // This is platform specific: @@ -269,11 +282,6 @@ template struct SingletonInstance { static LoadTimeSingletonEntry g_entry; // we know scalar types are trivially destructible, but add check for intent static_assert(std::is_trivially_destructible_v); - -// Extra work required to make it platform independent, for now: -#if !defined(__GNUC__) && !defined(__clang__) -#error "LOAD_TIME singletons require GCC or Clang (uses __attribute__((destructor)))" -#endif }; template @@ -282,6 +290,7 @@ template T* SingletonInstance::g_pInte template LoadTimeSingletonEntry SingletonInstance::g_entry = { false, SingletonInstance::DestroyInternalInstance, nullptr }; +#endif // ENABLE_LOAD_TIME_SINGLETON } // namespace Detail From 5ae4812f41d36eb9df2e891a4d27fcbe6c2967d3 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 00:56:17 +0100 Subject: [PATCH 06/19] fix: macro guard --- include/singleton.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/singleton.hpp b/include/singleton.hpp index 21c769e..16db213 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -7,11 +7,13 @@ #include // More effort would be required to make load-time singletons work on MSVC +#ifndef ENABLE_LOAD_TIME_SINGLETON #if defined(__GNUC__) || defined(__clang__) #define ENABLE_LOAD_TIME_SINGLETON 1 #else #define ENABLE_LOAD_TIME_SINGLETON 0 #endif +#endif // ENABLE_LOAD_TIME_SINGLETON /// This file implements the singleton pattern, but makes unit testing easy. From 21358baf95306cab65879ffd8021855d3e9eedd3 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 00:57:47 +0100 Subject: [PATCH 07/19] fix: comment placement --- include/singleton.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 16db213..bef9225 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -6,6 +6,8 @@ #include #include +/// This file implements the singleton pattern, but makes unit testing easy. + // More effort would be required to make load-time singletons work on MSVC #ifndef ENABLE_LOAD_TIME_SINGLETON #if defined(__GNUC__) || defined(__clang__) @@ -15,8 +17,6 @@ #endif #endif // ENABLE_LOAD_TIME_SINGLETON -/// This file implements the singleton pattern, but makes unit testing easy. - // Types of singletons: enum class SingletonType : int32_t { // default case, 99% of the time this is enough From b2c018ac2674d71df095eff81975716b11b3bf73 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 01:01:29 +0100 Subject: [PATCH 08/19] fix: javadoc comment on public parts --- include/singleton.hpp | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index bef9225..5a96ee5 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -17,25 +17,33 @@ #endif #endif // ENABLE_LOAD_TIME_SINGLETON -// Types of singletons: +/** + * @brief Types of singletons + */ enum class SingletonType : int32_t { - // default case, 99% of the time this is enough + /** + * @brief Default case, 99% of the time this is enough + */ STATIC #if ENABLE_LOAD_TIME_SINGLETON , - // Some notes: - // Static instances don't always play nice with process termination: - // Problem with atexit handlers: - // - When a static instance is created (e.g. meyers singleton at first use) after - // the atexit handler is registered, then by the time the atexit handler runs, - // this static instance has already been destroyed. - // - This is not a problem if the static instance is created before the atexit handler - // is registered, but when your library is used by some third party or customer processes, - // you cannot rely on this. - // So a singleton type created with this LOAD_TIME parameter provides an alternative: - // - The instance is created at first use as usual. - // - However, its destruction is deferred until "unload time", when your library is unloaded, - // or if linked statically during process termination, but after static destruction phase. + /** + * @brief Load-time singleton: use for singleton that must outlive static destruction phase. + * + * @note + * Static instances don't always play nice with process termination: + * Problem with atexit handlers: + * - When a static instance is created (e.g. meyers singleton at first use) after + * the atexit handler is registered, then by the time the atexit handler runs, + * this static instance has already been destroyed. + * - This is not a problem if the static instance is created before the atexit handler + * is registered, but when your library is used by some third party or customer processes, + * you cannot rely on this. + * So a singleton type created with this LOAD_TIME parameter provides an alternative: + * - The instance is created at first use as usual. + * - However, its destruction is deferred until "unload time", when your library is unloaded, + * or if linked statically during process termination, but after static destruction phase. + */ LOAD_TIME #endif // ENABLE_LOAD_TIME_SINGLETON }; From 3324e3d796f36aea6bb4015b7c0459b438fdb3cd Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 12:17:33 +0100 Subject: [PATCH 09/19] fix: format --- include/singleton.hpp | 268 ++++++++++++++++++++---------------------- 1 file changed, 125 insertions(+), 143 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 5a96ee5..39e6fdc 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -6,8 +6,6 @@ #include #include -/// This file implements the singleton pattern, but makes unit testing easy. - // More effort would be required to make load-time singletons work on MSVC #ifndef ENABLE_LOAD_TIME_SINGLETON #if defined(__GNUC__) || defined(__clang__) @@ -15,7 +13,7 @@ #else #define ENABLE_LOAD_TIME_SINGLETON 0 #endif -#endif // ENABLE_LOAD_TIME_SINGLETON +#endif /** * @brief Types of singletons @@ -48,101 +46,12 @@ enum class SingletonType : int32_t { #endif // ENABLE_LOAD_TIME_SINGLETON }; -// fw declare some dependencies -namespace Detail { -template struct SingletonInstance; -} - -/** To use the Singleton class normally, inherit from it with the CRTP style, like: - * - * ```cpp - * class MyClass : public Singleton { impl... }; - * ``` - * - * After this, external code is able to get the `MyClass` instance by using `MyClass::Get()`. - * - * To test your singleton, an access-private library implementation is required, such as - * . Using the access-private library, invoke the - * `MyClass::Inject()` function to inject a mock implementation into the singleton. It is also - * possible to reconstruct the Singleton with different constructor arguments by using the - * `MyClass::Reset()` function. - * - * @remark The `Inject()` and `Reset()` functions are NOT thread safe! They should only be used - * in test code sections while implementation code is not running on a different thread. - */ -template struct Singleton { - using BaseType = Singleton; - - /// Returns the instance of the class. - /** @remark The constructor arguments are only used if the instance is not constructed yet. - */ - template static T& Get(Args&&... args); - - /// Returns the instance of the class without construction. - /** @return The instance of the singleton or a `nullptr` if unconstructed. - * @remark This can be useful for singletons that have custom constructor arguments. Code can - * get an already initialied instance, if it has been initialized already, without - * having to specify the arguments. - */ - static T* TryGet(); - -protected: - Singleton() noexcept = default; - -private: - using Instance = Detail::SingletonInstance; -#if ENABLE_LOAD_TIME_SINGLETON - // just here to catch future regressions - static_assert(type != SingletonType::LOAD_TIME || std::is_trivially_destructible_v, - "Load-time singletons must be trivially destructible"); -#endif // ENABLE_LOAD_TIME_SINGLETON - - Singleton(const Singleton&) = delete; - Singleton& operator=(const Singleton&) = delete; - - static Instance g_instance; - - /// Holds a flag used for `std::call_once()`. - struct OnceFlag final { - OnceFlag() noexcept; - OnceFlag(const OnceFlag&) = delete; - ~OnceFlag(); - OnceFlag& operator=(const OnceFlag&) = delete; - /// Resets the OnceFlag to initial state (allowing another call) - void Reset(); - /// Returns the internal std::once_flag. May be uninitialized. - operator std::once_flag&(); - - private: - union U { - std::once_flag asOnceFlag; - U() { } - ~U() { } - } buffer; - }; - - static OnceFlag g_onceFlag; - - /// (Re)constructs the internal singleton instance. - /** If an existing singleton instance was already constructed, it is destroyed. If an external - * instance was injected, it is overridden with the newly constructed instance. - * - * @remark This function is not thread safe. It is intended for tests, not production code. - */ - template static T& Reset(Args&&... args); - - /// Injects an external instance into the singleton. - /** If an external instance is injected, the `Get()` function - * @param object The object is taken without ownership and must be deleted by the caller. - * @remark If `object` is `nullptr`, it resets the singleton to uninitialized state, and the - * next invocation of `Get()` reconstructs the instance. - */ - static void Inject(T* object); -}; - // Specialiaze some details of the singleton class in "private" Detail namespace namespace Detail { +template +struct SingletonInstance; + // Static lifetime singleton specialization (default) template struct SingletonInstance { SingletonInstance() noexcept = default; @@ -205,13 +114,13 @@ T* const SingletonInstance::LOCAL_INSTANCE_ID = reinte // are called at "unload" time. using DeleteLoadTimeSingletonFunc = void (*)(); - struct LoadTimeSingletonEntry { bool m_isValid = true; // To delete or not (to avoid double free between Reset and __attribute__(destructor)) DeleteLoadTimeSingletonFunc m_deleteFunc; LoadTimeSingletonEntry* m_next; }; -// note to maintainer: all types used by load time singletons must be trivially destructible, +// note to maintainer: all types that have static storage +// used by load time singletons must be trivially destructible, // otherwise they will get destroyed during static destruction phase before "unload" time. static_assert(std::is_trivially_destructible_v); @@ -302,53 +211,126 @@ LoadTimeSingletonEntry SingletonInstance::g_entry = { false, SingletonInstance::DestroyInternalInstance, nullptr }; #endif // ENABLE_LOAD_TIME_SINGLETON -} // namespace Detail - -// Implementation of Singleton class - -template template T& Singleton::Get(Args&&... args) -{ - std::call_once( - g_onceFlag, [](Args&&... args) { g_instance.Emplace(std::forward(args)...); }, - std::forward(args)...); - return *static_cast(g_instance); } -template T* Singleton::TryGet() -{ - return g_instance; -} -template template T& Singleton::Reset(Args&&... args) -{ - g_onceFlag.Reset(); - return Get(std::forward(args)...); -} -template void Singleton::Inject(T* object) -{ - if (object) - std::call_once(g_onceFlag, []() {}); - else - g_onceFlag.Reset(); - g_instance.SetExtern(object); -} -template Singleton::OnceFlag::OnceFlag() noexcept -{ - new (&buffer.asOnceFlag) std::once_flag(); -} -template Singleton::OnceFlag::~OnceFlag() -{ - buffer.asOnceFlag.~once_flag(); -} -template void Singleton::OnceFlag::Reset() -{ - this->~OnceFlag(); - new (this) OnceFlag(); -} -template Singleton::OnceFlag::operator std::once_flag&() + +/// Implements the singleton pattern, but makes unit testing easy. +/** To use the Singleton class normally, inherit from it with the CRTP style, like: + * + * ```cpp + * class MyClass : public Singleton { impl... }; + * ``` + * + * After this, external code is able to get the `MyClass` instance by using `MyClass::Get()`. + * + * To test your singleton, an access-private library implementation is required, such as + * . Using the access-private library, invoke the + * `MyClass::Inject()` function to inject a mock implementation into the singleton. It is also + * possible to reconstruct the Singleton with different constructor arguments by using the + * `MyClass::Reset()` function. + * + * @remark The `Inject()` and `Reset()` functions are NOT thread safe! They should only be used + * in test code sections while implementation code is not running on a different thread. + */ +template +struct Singleton { - return buffer.asOnceFlag; -} + using BaseType = Singleton; + + /// Returns the instance of the class. + /** @remark The constructor arguments are only used if the instance is not constructed yet. + */ + template + static T& Get(Args... args) + { + std::call_once(g_onceFlag, [](Args... args) { + g_instance.Emplace(std::forward(args)...); + }, args...); + return *static_cast(g_instance); + } + + /// Returns the instance of the class without construction. + /** @return The instance of the singleton or a `nullptr` if unconstructed. + * @remark This can be useful for singletons that have custom constructor arguments. Code can + * get an already initialied instance, if it has been initialized already, without + * having to specify the arguments. + */ + static T* TryGet() + { + return g_instance; + } + +protected: + Singleton() noexcept = default; +private: + using Instance = Detail::SingletonInstance; +#if ENABLE_LOAD_TIME_SINGLETON + // just here to catch future regressions + static_assert(type != SingletonType::LOAD_TIME || std::is_trivially_destructible_v, + "Load-time singletons must be trivially destructible"); +#endif -template typename Singleton::OnceFlag Singleton::g_onceFlag; -template typename Singleton::Instance Singleton::g_instance; + Singleton(const Singleton&) = delete; + Singleton& operator =(const Singleton&) = delete; + + static Instance g_instance; -#endif // TESTABLE_SINGLETON_INCLUDED_H + /// Holds a flag used for `std::call_once()`. + static struct OnceFlag final + { + OnceFlag() noexcept + { + new (&buffer.asOnceFlag) std::once_flag(); + } + OnceFlag(const OnceFlag&) = delete; + ~OnceFlag() + { + buffer.asOnceFlag.~once_flag(); + } + OnceFlag& operator =(const OnceFlag&) = delete; + /// Resets the OnceFlag to initial state (allowing another call) + void Reset() + { + this->~OnceFlag(); + new (this) OnceFlag(); + } + /// Returns the internal std::once_flag. May be uninitialized. + operator std::once_flag& () + { + return buffer.asOnceFlag; + } + private: + union U { std::once_flag asOnceFlag; U(){} ~U(){} } buffer; + } g_onceFlag; + + /// (Re)constructs the internal singleton instance. + /** If an existing singleton instance was already constructed, it is destroyed. If an external + * instance was injected, it is overridden with the newly constructed instance. + * + * @remark This function is not thread safe. It is intended for tests, not production code. + */ + template + static T& Reset(Args... args) + { + g_onceFlag.Reset(); + return Get(std::forward(args)...); + } + + /// Injects an external instance into the singleton. + /** If an external instance is injected, the `Get()` function + * @param object The object is taken without ownership and must be deleted by the caller. + * @remark If `object` is `nullptr`, it resets the singleton to uninitialized state, and the + * next invocation of `Get()` reconstructs the instance. + */ + static void Inject(T* object) + { + if (object) + std::call_once(g_onceFlag, []() {}); + else + g_onceFlag.Reset(); + g_instance.SetExtern(object); + } +}; +template +typename Singleton::OnceFlag Singleton::g_onceFlag; +template +typename Singleton::Instance Singleton::g_instance; From 279a7cd42f756d9b363014a123959331a75ef453 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 12:23:55 +0100 Subject: [PATCH 10/19] fix: comments --- include/singleton.hpp | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 39e6fdc..4965bc1 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -92,11 +92,7 @@ template struct SingletonInstance { static T& GetBuffer() { // Static, uninitialized buffer for the singleton's object - static union U { - T asT; - U() { } - ~U() { } - } buffer; + static union U { T asT; U(){} ~U(){} } buffer; return buffer.asT; } }; @@ -162,7 +158,9 @@ inline void __attribute__((destructor)) DestroyLoadTimeSingletons() // API is compatible with static singleton specialization // however it has no custom dtor (as that would violate trivial destructibility) template struct SingletonInstance { + /// Returns the current instance. operator T*() { return m_pExtern == LOCAL_INSTANCE_ID ? g_pInternal : m_pExtern; } + /// Constructs the singleton on the heap, and pushes its deleter to the unload-time stack. template void Emplace(Args&&... args) { Reset(); @@ -170,6 +168,9 @@ template struct SingletonInstance { RegisterLoadTimeSingleton(&g_entry); m_pExtern = LOCAL_INSTANCE_ID; } + /// Sets an external object as the instance. + /** @remark If `ptr` is `nullptr`, this is just reset. + */ void SetExtern(T* ptr) { Reset(); @@ -180,6 +181,7 @@ template struct SingletonInstance { // Swapped the dtor on static instance for a Reset function void Reset() { + // Destroys the locally-initialized instance. Injected ones are ignored (no ownership). if (m_pExtern == LOCAL_INSTANCE_ID) DestroyInternalInstance(); } @@ -195,9 +197,14 @@ template struct SingletonInstance { g_entry.m_isValid = false; } + /// A special pointer value to specify that the local buffer is constructed. static T* const LOCAL_INSTANCE_ID; + /// nullptr if empty; LOCAL_INSTANCE_ID if using internal buffer; + /// pointer to external object otherwise. T* m_pExtern = nullptr; + /// used instead of static buffer static T* g_pInternal; + /// used for deleting this singleton static LoadTimeSingletonEntry g_entry; // we know scalar types are trivially destructible, but add check for intent static_assert(std::is_trivially_destructible_v); @@ -240,9 +247,9 @@ struct Singleton /** @remark The constructor arguments are only used if the instance is not constructed yet. */ template - static T& Get(Args... args) + static T& Get(Args&&... args) { - std::call_once(g_onceFlag, [](Args... args) { + std::call_once(g_onceFlag, [](Args&&... args) { g_instance.Emplace(std::forward(args)...); }, args...); return *static_cast(g_instance); @@ -309,7 +316,7 @@ struct Singleton * @remark This function is not thread safe. It is intended for tests, not production code. */ template - static T& Reset(Args... args) + static T& Reset(Args&&... args) { g_onceFlag.Reset(); return Get(std::forward(args)...); @@ -334,3 +341,5 @@ template typename Singleton::OnceFlag Singleton::g_onceFlag; template typename Singleton::Instance Singleton::g_instance; + +#endif From b1cc2cedf1e3fa07c70cbdd2dafef56dec02e5a8 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 12:25:49 +0100 Subject: [PATCH 11/19] fix: format --- include/singleton.hpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 4965bc1..c17d5d1 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -53,7 +53,8 @@ template struct SingletonInstance; // Static lifetime singleton specialization (default) -template struct SingletonInstance { +template +struct SingletonInstance { SingletonInstance() noexcept = default; SingletonInstance(const SingletonInstance&) = delete; ~SingletonInstance() @@ -66,7 +67,8 @@ template struct SingletonInstance { /// Returns the current instance. operator T*() { return m_pExtern == LOCAL_INSTANCE_ID ? &GetBuffer() : m_pExtern; } /// Constructs the singleton within the local buffer. - template void Emplace(Args&&... args) + template + void Emplace(Args&&... args) { this->~SingletonInstance(); new (&GetBuffer()) T(std::forward(args)...); @@ -157,11 +159,13 @@ inline void __attribute__((destructor)) DestroyLoadTimeSingletons() // Load-time singleton specialization // API is compatible with static singleton specialization // however it has no custom dtor (as that would violate trivial destructibility) -template struct SingletonInstance { +template +struct SingletonInstance { /// Returns the current instance. operator T*() { return m_pExtern == LOCAL_INSTANCE_ID ? g_pInternal : m_pExtern; } /// Constructs the singleton on the heap, and pushes its deleter to the unload-time stack. - template void Emplace(Args&&... args) + template + void Emplace(Args&&... args) { Reset(); CreateInternalInstance(std::forward(args)...); @@ -186,7 +190,8 @@ template struct SingletonInstance { DestroyInternalInstance(); } // Manage internal instance on heap: - template static void CreateInternalInstance(Args&&... args) + template + static void CreateInternalInstance(Args&&... args) { g_pInternal = new T(std::forward(args)...); g_entry.m_isValid = true; @@ -212,7 +217,8 @@ template struct SingletonInstance { template T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(0x1); -template T* SingletonInstance::g_pInternal = nullptr; +template +T* SingletonInstance::g_pInternal = nullptr; template LoadTimeSingletonEntry SingletonInstance::g_entry = { false, SingletonInstance::DestroyInternalInstance, nullptr }; From d1a8911dc5efab1e0c994169f3a49e57c35a9df8 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 12:26:28 +0100 Subject: [PATCH 12/19] fix: num literal --- include/singleton.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index c17d5d1..841c63c 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -100,7 +100,7 @@ struct SingletonInstance { }; template -T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(0x1); +T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(1); #if ENABLE_LOAD_TIME_SINGLETON // Load time singleton impl: @@ -216,7 +216,7 @@ struct SingletonInstance { }; template -T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(0x1); +T* const SingletonInstance::LOCAL_INSTANCE_ID = reinterpret_cast(1); template T* SingletonInstance::g_pInternal = nullptr; template From a9a17b2c6fd73208bc1d540da03f7a46efd2a01e Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 12:27:36 +0100 Subject: [PATCH 13/19] feat: update comments --- include/singleton.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 841c63c..39725d6 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -13,7 +13,7 @@ #else #define ENABLE_LOAD_TIME_SINGLETON 0 #endif -#endif +#endif // ENABLE_LOAD_TIME_SINGLETON /** * @brief Types of singletons @@ -224,7 +224,7 @@ LoadTimeSingletonEntry SingletonInstance::g_entry = { false, SingletonInstance::DestroyInternalInstance, nullptr }; #endif // ENABLE_LOAD_TIME_SINGLETON -} +} // namespace Detail /// Implements the singleton pattern, but makes unit testing easy. /** To use the Singleton class normally, inherit from it with the CRTP style, like: @@ -280,7 +280,7 @@ struct Singleton // just here to catch future regressions static_assert(type != SingletonType::LOAD_TIME || std::is_trivially_destructible_v, "Load-time singletons must be trivially destructible"); -#endif +#endif // ENABLE_LOAD_TIME_SINGLETON Singleton(const Singleton&) = delete; Singleton& operator =(const Singleton&) = delete; From 6ee1bfe2571081678715d9a86e1790df37b78750 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 12:29:35 +0100 Subject: [PATCH 14/19] fix: redundant init --- include/singleton.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 39725d6..53df6bd 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -113,7 +113,7 @@ T* const SingletonInstance::LOCAL_INSTANCE_ID = reinte using DeleteLoadTimeSingletonFunc = void (*)(); struct LoadTimeSingletonEntry { - bool m_isValid = true; // To delete or not (to avoid double free between Reset and __attribute__(destructor)) + bool m_isValid; // To delete or not (to avoid double free between Reset and __attribute__(destructor)) DeleteLoadTimeSingletonFunc m_deleteFunc; LoadTimeSingletonEntry* m_next; }; From ee8566a13d408751d3bc7282480a4b348211cfbb Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 14:30:58 +0100 Subject: [PATCH 15/19] feat: separate implementation om MSVC --- include/singleton.hpp | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 53df6bd..f0a6502 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -6,9 +6,13 @@ #include #include -// More effort would be required to make load-time singletons work on MSVC #ifndef ENABLE_LOAD_TIME_SINGLETON -#if defined(__GNUC__) || defined(__clang__) +#if defined(__GNUC__) || \ + defined(__clang__) || \ + defined(__MINGW32__) || \ + defined(__MINGW64__) || \ + defined(__CYGWIN__) || \ + defined(_MSC_VER) #define ENABLE_LOAD_TIME_SINGLETON 1 #else #define ENABLE_LOAD_TIME_SINGLETON 0 @@ -146,8 +150,9 @@ inline void RegisterLoadTimeSingleton(LoadTimeSingletonEntry* entry) entry->m_next, entry, std::memory_order_release, std::memory_order_relaxed)) ; } -// attribute destructor to make sure its run after static destruction phase -inline void __attribute__((destructor)) DestroyLoadTimeSingletons() + +// - Platform independent part: delete in reverse order +inline void DestroyLoadTimeSingletons() { // memory safety is no concern here for (auto entry = LoadTimeSingletonEntryStack().load(); entry != nullptr; entry = entry->m_next) { @@ -156,6 +161,23 @@ inline void __attribute__((destructor)) DestroyLoadTimeSingletons() } } +#if defined(_MSC_VER) +// MSVC: Use .CRT$XPU section for late destruction +typedef void (__cdecl *_PVFV)(void); +static void __cdecl DeinitializeLoadTimeSingletons() +{ + DestroyLoadTimeSingletons(); +} +#pragma section(".CRT$XPU", long, read) +__declspec(allocate(".CRT$XPU")) _PVFV p_deinit = DeinitializeLoadTimeSingletons; +#else +// GCC/Clang/CYGWIN/MINGW: Use __attribute__((destructor)) +inline void __attribute__((destructor)) DeinitializeLoadTimeSingletons() +{ + DestroyLoadTimeSingletons(); +} +#endif + // Load-time singleton specialization // API is compatible with static singleton specialization // however it has no custom dtor (as that would violate trivial destructibility) From 0c1177e8a7f71147dead2b311969de6e7c265dbe Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 14:33:33 +0100 Subject: [PATCH 16/19] fix: parameter pack forwarding --- include/singleton.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index ed0f9b0..7f9cae5 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -18,7 +18,7 @@ * `MyClass::Inject()` function to inject a mock implementation into the singleton. It is also * possible to reconstruct the Singleton with different constructor arguments by using the * `MyClass::Reset()` function. - * + * * @remark The `Inject()` and `Reset()` functions are NOT thread safe! They should only be used * in test code sections while implementation code is not running on a different thread. */ @@ -31,11 +31,11 @@ struct Singleton /** @remark The constructor arguments are only used if the instance is not constructed yet. */ template - static T& Get(Args... args) + static T& Get(Args&&... args) { - std::call_once(g_onceFlag, [](Args... args) { + std::call_once(g_onceFlag, [](Args&&... args) { g_instance.Emplace(std::forward(args)...); - }, args...); + }, std::forward(args)...); return *static_cast(g_instance); } @@ -72,7 +72,7 @@ struct Singleton operator T* () { return m_pExtern == LOCAL_INSTANCE_ID ? &GetBuffer() : m_pExtern; } /// Constructs the singleton within the local buffer. template - void Emplace(Args... args) + void Emplace(Args&&... args) { this->~Instance(); new (&GetBuffer()) T(std::forward(args)...); @@ -132,18 +132,18 @@ struct Singleton /// (Re)constructs the internal singleton instance. /** If an existing singleton instance was already constructed, it is destroyed. If an external * instance was injected, it is overridden with the newly constructed instance. - * + * * @remark This function is not thread safe. It is intended for tests, not production code. */ template - static T& Reset(Args... args) + static T& Reset(Args&&... args) { g_onceFlag.Reset(); return Get(std::forward(args)...); } /// Injects an external instance into the singleton. - /** If an external instance is injected, the `Get()` function + /** If an external instance is injected, the `Get()` function * @param object The object is taken without ownership and must be deleted by the caller. * @remark If `object` is `nullptr`, it resets the singleton to uninitialized state, and the * next invocation of `Get()` reconstructs the instance. From c2ff8107a1654a48b8f474e65e641a91c2609f35 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 14:41:54 +0100 Subject: [PATCH 17/19] fix: static -> inline func --- include/singleton.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 4277677..2d33020 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -164,7 +164,7 @@ inline void DestroyLoadTimeSingletons() #if defined(_MSC_VER) // MSVC: Use .CRT$XPU section for late destruction typedef void (__cdecl *_PVFV)(void); -static void __cdecl DeinitializeLoadTimeSingletons() +inline void __cdecl DeinitializeLoadTimeSingletons() { DestroyLoadTimeSingletons(); } From bfc1ac843df7b937d141ed7d94d5130cbc11a057 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 14:46:38 +0100 Subject: [PATCH 18/19] fix: remove cpp17 features --- include/singleton.hpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 2d33020..801e962 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -121,15 +121,10 @@ struct LoadTimeSingletonEntry { DeleteLoadTimeSingletonFunc m_deleteFunc; LoadTimeSingletonEntry* m_next; }; -// note to maintainer: all types that have static storage -// used by load time singletons must be trivially destructible, -// otherwise they will get destroyed during static destruction phase before "unload" time. -static_assert(std::is_trivially_destructible_v); // global stack for load time singletons dtors inline std::atomic& LoadTimeSingletonEntryStack() { - static_assert(std::is_trivially_destructible_v>); static std::atomic stack { nullptr }; return stack; } @@ -233,8 +228,6 @@ struct SingletonInstance { static T* g_pInternal; /// used for deleting this singleton static LoadTimeSingletonEntry g_entry; - // we know scalar types are trivially destructible, but add check for intent - static_assert(std::is_trivially_destructible_v); }; template @@ -299,7 +292,10 @@ struct Singleton private: using Instance = Detail::SingletonInstance; #if ENABLE_LOAD_TIME_SINGLETON - // just here to catch future regressions + // note to maintainer: Load time singleton instance must be trivially destructible, + // otherwise it (or some of its members) will get destroyed during static destruction + // phase before "unload" time. + // this remains here to catch future regressions static_assert(type != SingletonType::LOAD_TIME || std::is_trivially_destructible_v, "Load-time singletons must be trivially destructible"); #endif // ENABLE_LOAD_TIME_SINGLETON From 4d2be08314891627605d74c5d8675732666ab3c4 Mon Sep 17 00:00:00 2001 From: "benjamin.varju" Date: Mon, 24 Nov 2025 15:13:02 +0100 Subject: [PATCH 19/19] fix: remove cpp17 feature --- include/singleton.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/singleton.hpp b/include/singleton.hpp index 801e962..33e8812 100644 --- a/include/singleton.hpp +++ b/include/singleton.hpp @@ -296,7 +296,8 @@ struct Singleton // otherwise it (or some of its members) will get destroyed during static destruction // phase before "unload" time. // this remains here to catch future regressions - static_assert(type != SingletonType::LOAD_TIME || std::is_trivially_destructible_v, + static_assert(type != SingletonType::LOAD_TIME || + std::is_trivially_destructible::value, "Load-time singletons must be trivially destructible"); #endif // ENABLE_LOAD_TIME_SINGLETON