每个对象单例

Per-object singleton

(我尝试搜索,但你只会得到大量简单的单例解释。)

“普通”单例保证在整个程序中只存在一个给定类型的对象。例如,像这样:

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

我正在寻找一种方法,在 O 类型的任何给定对象 中,给定类型 T 的对象不超过一个。也就是说,像这样:

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() { /* ??? */ }
};

如果所有 T 都需要从某个基数 class 派生,这完全没问题(这可能是将它们类型擦除存储在 O 内的容器中所必需的) ,比如说,TBase。但是,我们关心的 T 集合是开放的(即在定义 O 的地方不知道),因此成员变量不是解决方案。

当然有或多或少直接的存储解决方案,例如a std::vector<std::unique_ptr<TBase>> 并通过在每个元素上尝试 dynamic_cast 来实现 getSingleton<T>。但我想知道是否有更优雅的方法,不需要线性时间。

标准库提供了std::type_index,可用于有效地将唯一值关联到每个类型。此类型旨在用作关联容器的键。 std::unordered_map<std::type_index, std::any> 可用于包含任何类型的集合。由于知道该映射中任何元素的原始类型需要知道原始类型(以构造正确的 std::type_index 键),因此应该始终可以成功 any_cast 值。

这是一个示例实现 (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;
};

用法如下:

#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';
}

此解决方案对T的唯一要求是它默认可构造和可破坏。

可以使用类似这样的东西:

#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());
  }
};