如何创建一个行为几乎与 int64_t 完全相同但不允许句柄类型之间的隐式转换的类型安全句柄?

How can I create a typesafe handle that behaves almost exactly like int64_t, but disallow implicit conversions between handle types?

上下文:

我创建了一个名为“ComponentManager”的通用容器。它看起来大致是这样的:

typedef uint64_t handle;

template<typename T>
class ComponentManager {
    std::vector<T> components; // stores the elements themselves
    std::unordered_map<handle, unsigned int> handles; // maps handles to indices
    HandleGenerator handleGenerator; // class that generates unique handles
public:
    size_t size() const;

    handle add(T component); // Adds an element to the component manager, and returns the handle for that element

    T* find(handle key) const; // returns a pointer to the element refered to by this handle, or NULL if none exists

    void remove(handle key); // may invalidate all iterators. Does NOT invalidate any key (except the one to the element deleted)

    ComponentManagerIterator<T> begin();
    ComponentManagerIterator<T> end();
};

这个 class 使用 handles -> indices 的哈希映射来允许 O(1) 随机访问元素,同时由于元素在向量中的空间局部性 (虽然不能保证特定的顺序,因为删除元素时元素可能会被打乱)

问题:

就目前而言,程序中的所有句柄都具有相同的类型,因此用户可能会错误地尝试使用与一个句柄管理器对应的句柄来访问完全不相关的句柄管理器中的元素。

我想根据 handlemanager 的类型为句柄本身提供不同的类型来缓解这种情况。类似于此:

template<typename T>
class handle
{
public:
    uint64_t key;
    explicit handle(uint64_t key_) : key(key_) {}
}

这样一来,如果您尝试使用错误类型的 handlemanager 的句柄,如果没有显式转换,代码将无法编译。

然而,问题在于我有时仍想将这些句柄视为整数。我希望为这种类型定义所有常规整数运算(比较、按位运算等),并且我希望专门用于整数的算法(例如 std::hash)像我的类型 [=35] 一样运行=]是一个整数。有没有一种方法可以做到这一点,而无需我自己手动实施这些操作中的每一个?

如果有更好的方法以不同的方式获得像这样的类型安全,我愿意接受其他实现此目的的方法。

编辑:我还应该提到,在我的程序中,任何给定类型 T 也只会有一个组件管理器,因此仅此类型就足以识别特定的组件管理器。

编辑 2(附加上下文):我在赋予句柄独特类型时看到的一个好处是,我可以重载单个函数以根据句柄的类型访问不同的组件管理器。

您可以创建一个方法来进行从 handleuint64_t 的隐式转换。

template<typename T>
class handle
{
public:
    operator uint64_t() { return key_; }
    explicit handle(uint64_t key) : key_(key) {}
private:
    uint64_t key_;
}

这会在上下文需要时自动将 handle<T> 转换为 uint64_t

#include <iostream>
#include <string>

template<typename T>
class handle
{
public:
    operator uint64_t() { return key_; }
    explicit handle(uint64_t key) : key_(key) {}
private:
    uint64_t key_;
};

template<typename T>
int plus_20(T t)
{
    return t + 20;
}

int main()
{
  handle<int> hand(4);
  std::cout << hand << std::endl; // 4
  std::cout << hand + 1 << std::endl; // 5
  std::cout << (hand << 3) << std::endl; // 32
  std::cout << plus_20(hand) << std::endl; // 24
  //std::cout << plus_20<std::string>(hand) << std::endl; // doesn't compile

  std::unordered_map<uint64_t, std::string> umap;
  umap[hand] = "test";
  for(auto [key, value] : umap)
  {
      std::cout << key << " --> " << value << std::endl;
  }
}

现在,您的 class 可以如下所示(跳过未更改的部分):

template<typename T>
class ComponentManager {
    // ...
    std::unordered_map<uint64_t, unsigned int> handles; // maps handles to indices
    // ...
public:
    // ...
    handle<T> add(T component); // Adds an element to the component manager, and returns the handle for that element

    T* find(handle<T> key) const; // returns a pointer to the element refered to by this handle, or NULL if none exists

    void remove(handle<T> key); // may invalidate all iterators. Does NOT invalidate any key (except the one to the element deleted)
    // ...
};

请注意 ComponentManager class 中的 std::unordered_mapuint64_t 作为其键。 public 方法 add()find()remove() 的参数和 return 值确保类型安全。 handle<T> 构造函数上的 explicit 指定做了大量工作以确保一种句柄不能隐式转换为另一种句柄。


添加完全黑盒 handle 类型:

如果您想将 unordered_map 键保留为 handle<T>,您可以这样做而不必定义所有必需的操作。只需告诉 unordered_map 构造函数使用哪些:

template<typename T>
class ComponentManager {
    // ...
    std::unordered_map<handle<T>,
                       unsigned int,
                       std::hash<uint64_t>,
                       std::equal_to<uint64_t>> handles; // maps handles to indices
    // ...
};

如果将 public using key_type = uint64_t; 添加到 handle<T> class 模板,这可以推广到

template<typename T>
class ComponentManager {
    // ...
    std::unordered_map<handle<T>,
                       unsigned int,
                       std::hash<typename handle<T>::key_type>,
                       std::equal_to<typename handle<T>::key_type>> handles; // maps handles to indices
    // ...
};

这允许在 handle<T> class 模板中更改日期,而无需更新任何其他代码。