Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Per-object singleton

Tags:

c++

singleton

(I tried searching, but you just get a flood of plain singleton explanations.)

A "normal" singleton guarantees that only one object of a given type exists in the entire program. For example, like this:

template <class T>
T& getSingleton()
{
  static T instance;
  return instance;
}

I am looking for a way to have no more than one object of a given type T in any given object of type O. That is, something like this:

class O
{
  // getSingleton<SomeType>() always returns the same SomeType reference for the same instance of O.
  // getSingleton<SomeType>() returns different SomeType references for different instances of O.
  template<class T>
  T& getSingleton() { /* ??? */ }
};

It is totally fine if all T are required to derive from a certain base class (which is probably required for storing them type-erased in a container inside O), say, TBase. However, the set of T we care about is open (i.e. not known at the point where O is defined), hence member variables are not a solution.

There is of course the more or less straightforward solution of storing e.g. a std::vector<std::unique_ptr<TBase>> and implementing getSingleton<T> by attempting a dynamic_cast on each element. But I wonder if there is a more elegant approach that does not take linear time.

like image 313
Max Langhof Avatar asked Sep 03 '26 23:09

Max Langhof


1 Answers

The standard library provides std::type_index which can be used to effectively associated a unique value to every type. This type is designed to be usable as a key for associative containers. A std::unordered_map<std::type_index, std::any> can be used to contain a collection of any types. And the since knowing the original type of any element in that map requires knowing the original type (to construct the right std::type_index key) it should always be possible to successfully any_cast the value.

Here is an example implementation (godbolt) :

#include <any>
#include <typeindex>
#include <unordered_map>
    
class O
{
public:
    template<class T>
    T& getSingleton()
    { 
        // Get a unique key for the type `T`
        const auto key = std::type_index(typeid(T));

        // Check if the element already exists
        // If it doesn't, construct it in place
        // In either case, returns an iterator to the object
        auto[iter, unused] = members.try_emplace(key, std::in_place_type<T>);

        // Get a reference to the value using its original type
        return std::any_cast<T&>(iter->second);
    }    

private:
    std::unordered_map<std::type_index, std::any> members;
};

And the usage looks like :

#include <iostream>

int main()
{
    O o;

    // Will value initialize an `int`
    int & i = o.getSingleton<int>();
    
    // `i` and `j` refer to the same object
    int & j = o.getSingleton<int>();
    std::cout << i << ' ' << j << '\n';

    // Changing `i` changes `j`
    i = 42;
    std::cout << i << ' ' << j << '\n';
}

The only requirements that this solution imposes onT is that it is default constructible and destructible.

like image 63
4 revsFrançois Andrieux Avatar answered Sep 06 '26 12:09

4 revsFrançois Andrieux



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!