(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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With