Per-object singleton

Viewed 116

(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.

2 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.

Something like this could be used:

#include <map>     // for map
#include <memory>  // for unique_ptr
#include <tuple>   // for ignore, tie

class O {
 private:
  template <class T>
  static void index(){};
  using obj_ptr = std::unique_ptr<void, void (*)(void*) noexcept>;
  std::map<void (*)(), obj_ptr> instances;

 public:
  template <class T>
  T& getSingleton() {
    auto it = instances.find(index<T>);
    if (it == instances.end()) {
      static constexpr auto deleter =
          +[](void* t) noexcept { delete static_cast<T*>(t); };

      std::tie(it, std::ignore) =
          instances.emplace(std::pair{&index<T>, obj_ptr{new T, deleter}});
    }
    return *static_cast<T*>(it->second.get());
  }
};
Related