singleton and singleton_control

Overview

Cbeam’s singleton<T, ...> and singleton_control collectively define a robust way to create, manage, and explicitly destroy singletons at runtime. Each singleton resource is held in a std::shared_ptr<T>, returned via singleton<T, ...>::get(name, args...). Unlike typical static singletons, this approach:

  1. Allows multiple named instances of the same type (each identified by a std::string key).
  2. Guarantees clean destruction at a chosen time rather than relying on the indeterminate order of static destructors.
  3. Provides global reset via singleton_control::reset() for all active singletons, plus a mechanism to re-enable creation later (set_operational()).
  4. Ensures type safety: If a singleton of a different type or signature is requested under the same name, it will throw a runtime error.

All of this is achieved in a thread-safe manner, using an internal std::mutex.


Motivation & Differences vs. Traditional Singletons

Traditional static singletons are often destroyed at program exit in an unspecified order. This can be problematic if the destructor depends on other singletons that might already be torn down. By contrast, singleton<T>:

  • Gives you explicit control over when you want to destruct everything (e.g., in tests or plugin reloads).
  • Manages references via std::shared_ptr<T> so any code holding the resource can continue using it until it naturally expires.

Without an explicit mechanism like cbeam::lifecycle::singleton, you might be forced to rely on static objects, e.g. static Foo fooInstance, which is destroyed when the program ends, in an uncertain order.


Key Classes

singleton_control

A base struct that tracks all singletons in a global map:

static inline std::map<std::string, std::unique_ptr<singleton_control>> _instances;
static inline std::mutex _mutex;
static inline bool _shutdown{false};
  • reset()
    Destroys all stored singletons (by calling each subclass’s release_instance()), clears the map, and sets _shutdown = true.
  • After reset(), attempts to create or fetch any singleton will return nullptr until set_operational() is called.

  • set_operational()
    Re-enables singleton creation, setting _shutdown = false.

singleton<T, Args...>

A templated class deriving from singleton_control. It manages a single resource of type T, constructed by forwarding Args... to T’s constructor. Every instance is also bound to a name (string key).

Construction

The user never creates singleton<T> directly. Instead, they call:

auto ptr = singleton<T, Args...>::get("SomeName", arg1, arg2, ...);

If the singleton for "SomeName" doesn’t exist, it’s created. Otherwise, the existing one is returned.

Shared ownership

Internally, each singleton<T> object holds a std::shared_ptr<T> _instance;. Hence, a std::shared_ptr<T> is returned, which you can store and use as you would any shared resource. The global map stores a std::unique_ptr<singleton_control> referencing that same singleton<T>.
Additional calls to get("SomeName") return the same std::shared_ptr<T> (i.e., not a copy of the object, but a new pointer to the same instance).

Global or Partial Release

  • Full: singleton_control::reset() tears down all singletons, clearing the global map.
  • Per-name: singleton<T>::release("SomeName") removes just that named singleton from the map, calling its release_instance() internally.

Lifecycle

  • If you still hold a std::shared_ptr<T> after the singleton was released from the map, you retain a valid reference until you drop it, but the library no longer tracks it as a “named” singleton.
  • Once the last external reference goes out of scope, the object is destroyed in a deterministic manner.

Type Safety

If a singleton named "Foo" of type singleton<A> is in the map, and you accidentally call singleton<B>::get("Foo"), you will get an immediate runtime_error, preventing unsafe usage.


Usage Example

Creating and Accessing Singletons

#include <cbeam/lifecycle/singleton.hpp>
#include <iostream>

class MyResource {
public:
    int getValue() const { return _value; }
    void setValue(int v) { _value = v; }

private:
    int _value;
};

int main() {
    // Acquire or create named singleton. 
    // 'myResource' points to the same underlying MyResource if we call get("Example") again.
    std::shared_ptr<MyResource> myResource1 = cbeam::lifecycle::singleton<MyResource>::get("Example");

    myResource1->setValue(999);

    // Retrieve the same singleton by name:
    std::shared_ptr<MyResource> myResource2 = cbeam::lifecycle::singleton<MyResource>::get("Example");
    std::cout << "myResource2->getValue(): " << myResource2->getValue() << "\n"; 
    // prints 999

    return 0;
}

In this example:

  • First call to get("Example) constructs a new MyResource instance.
  • Subsequent calls to get("Example") return the same std::shared_ptr<MyResource> (no new construction).

Example: Full Test Flow

#include <cbeam/lifecycle/singleton.hpp>
#include <cassert>
#include <iostream>

class MyResource {
public:
    int getValue() const { return _value; }
    void setValue(int v) { _value = v; }

private:
    int _value{0};
};

int main() {
    {
        // Create one instnce
        auto inst = cbeam::lifecycle::singleton<MyResource>::get("ResourceA");
        inst->setValue(42);
    }

    {
        // The singleton "ResourceA" still exists in the global map, 
        // even though 'inst'  went out of scope.
        auto inst1 = cbeam::lifecycle::singleton<MyResource>::get("ResourceA");
        std::cout << "(A) inst1->getValue(): " << inst1->getValue() << std::endl; // prints 42

        // We can forcibly remove it:
        cbeam::lifecycle::singleton<MyResource>::release("ResourceA");

        // Now "ResourceA" is no longer managed by `cbeam::lifecycle::singleton`, but since it
        // is still hold by `inst1`, the resource is not yet destroyed:
        std::cout << "(B) inst1->getValue(): " << inst1->getValue() << std::endl; // prints 42
    }

    // Now "ResourceA" finally is destroyed, because it is neither referenced by
    // `cbeam::lifecycle::singleton`, nor by `inst1` any more.

    {
        auto inst2 = cbeam::lifecycle::singleton<MyResource>::get("ResourceA");
        std::cout << "(C) inst2->getValue(): " << inst2->getValue() << std::endl; // prints 0
        inst2->setValue(42);

        // Remove all referenced that are managed by `cbeam::lifecycle::singleton`
        // and prevent further construction (usually to exit the application):
        cbeam::lifecycle::singleton_control::reset();

        // `inst2` still works because it holds the last reference
        std::cout << "(D) inst2->getValue(): " << inst2->getValue() << std::endl; // prints 42
    }

    // Because of above call to `reset`, no new singletons can be created:
    auto test = cbeam::lifecycle::singleton<MyResource>::get("Another");
    std::cout << "(E) test != nullptr: " << (test ? "true" : "false") << std::endl; // false

    // Re-enable creation:
    cbeam::lifecycle::singleton_control::set_operational();
    auto test2 = cbeam::lifecycle::singleton<MyResource>::get("Another");
    std::cout << "(F) test != nullptr: " << (test2 ? "true" : "false") << std::endl; // true

    return 0;
}

Thread Safety

All operations on singletons are guarded by a static std::mutex. This ensures that:

  • Creation, retrieval, or release of singletons never collides in unpredictable ways.
  • Checking _shutdown and updating the map of singletons is atomic with respect to other threads.

When to Use

Scenarios

  1. Complex Global Resources
    E.g., logging managers, database connectors, GPU contexts—resources that must be shut down in a known sequence, or re-initialized between tests.

  2. Test Environments
    Let each test run in a “clean” environment by calling: cpp cbeam::lifecycle::singleton_control::reset(); cbeam::lifecycle::singleton_control::set_operational(); This ensures no stale global state from a previous test.

  3. Plugin Reloads
    If your application loads/unloads plugins (which might rely on global singletons), you can force-destroy them upon unloading.

Benefits vs. Basic std::unique_ptr Singletons

  • Global Discovery: By naming the singleton, any part of your code can fetch it.
  • Shared Ownership: You can safely pass around a std::shared_ptr<T> to multiple threads.
  • Explicit Control: Destruction order is entirely determined by you.

Summary

  • singleton_control:
  • reset() to clear everything.
  • set_operational() to allow creation again.

  • singleton<T, Args...>:

  • Creates or retrieves named singletons of type T.
  • Returns std::shared_ptr<T> for safe, reference-counted usage.
  • release(name) to drop a single named instance from the global map.

Together, these classes provide a robust, test-friendly mechanism for creating singletons that:

  • Don’t rely on C++ static object lifetimes.
  • Are easily reset or re-enabled on demand.
  • Are safe to use across multiple threads.
  • Offer typed, named resources that can’t be mistakenly reused under the wrong type.