C++编译时类型注册表实现:类型与递增ID双向映射的问题
C++编译时类型注册表实现:类型与递增ID双向映射的问题
我现在需要实现一个编译时的"注册表",能把注册的类型映射到递增的整数ID,同时也能通过ID反向查找到对应的类型。理想中的使用方式大概是这样的:
#define component(T) /* ?? */ template <int I> using registry_t = /* ?? */; component(some_type) { // some_type 被映射到ID 0 }; namespace my_namespace { component(some_type) { // my_namespace::some_type 被映射到ID 1 }; } static_assert(some_type::id() == 0, "First registered component gets ID 0"); static_assert(my_namespace::some_type::id() == 1, "Second should get ID 1"); static_assert(std::is_same_v<registry_t<0>, some_type>, "Should match"); static_assert(std::is_same_v<registry_t<1>, my_namespace::some_type>, "Should match");
我计划用falemagn的constexpr计数器库来实现这个任意类型到递增ID的映射,这个库的工作机制是:每次用唯一类型调用计数器的next方法时,就会分配一个递增的数字,示例代码如下:
struct __context; static constexpr fameta::counter<__context> component_counter; struct struct1; static_assert(component_counter.next<struct1>() == 0, "Starts at 0"); struct helloworld; static_assert(component_counter.next<helloworld>() == 1, "Increments to 1"); struct foobar; static_assert(component_counter.next<foobar>() == 2, "Increments to 2");
下面是我尝试过的几个实现方案:
尝试方案1:模板元编程
我第一个想到的常规实现方式是用模板特化:
template <int I> struct registry; template <int I> using registry_t = typename registry<I>::type; struct some_type {}; template <> struct registry<0> { using type = ::some_type; };
但这个方案有个致命的局限:component宏只能在全局命名空间(或者注册表模板所在的父命名空间)里使用,因为C++不允许在非父命名空间中特化模板,这直接限制了在子命名空间中注册类型的场景,不符合需求。
尝试方案2:重载技巧
为了绕开命名空间的限制,我想到了用重载的思路:先定义一个用int模板参数生成唯一类型的类,然后通过一个永远不会被实际调用的模板函数来充当注册表——这个函数接收unique<I>类型的参数,返回对应的注册类型。注册类型时,通过static_assert隐式特化这个模板函数。具体代码如下:
template <int> class unique final {}; template <class T> static constexpr T _registry(unique<component_counter.next<T>()>) noexcept; template <int I> using registry_t = std::remove_pointer_t<decltype(_registry(unique<I>{}))>; template <class T> struct component { static_assert(std::is_same_v<decltype(_registry<T>({})), T>); static constexpr int id() noexcept { return component_counter.next<T>(); } };
但这个方案编译失败了,用GCC 15.1编译时的错误信息如下:
<source>: In substitution of 'template<int I> using ecs::registry_t = std::remove_pointer_t<decltype (ecs::_registry(ecs::unique<I>{}))> [with int I = 0]': <source>:136:47: required from here 136 | static_assert(std::is_same_v<ecs::registry_t<0>, test>, ""); | ^ <source>:123:64: error: no matching function for call to '_registry(ecs::unique<0>)' 123 | using registry_t = std::remove_pointer_t<decltype(_registry(unique<I>{}))>; | ^~~~~~~~~ <source>:123:64: note: there is 1 candidate: <source>:121:24: note: candidate: 'template<class T> constexpr T ecs::_registry(unique<component_counter.next<T>()>)' 121 | static constexpr T _registry(unique<component_counter.next<T>()>) noexcept; | ^~~~~~~~~ <source>:121:24: note: template argument deduction/substitution failed: <source>:123:64: note: couldn't deduce template parameter 'T' 123 | using registry_t = std::remove_pointer_t<decltype(_registry(unique<I>{}))>; | ^~~~~~~~~ <source>:136:54: error: template argument 1 is invalid 136 | static_assert(std::is_same_v<ecs::registry_t<0>, test>, ""); | ^ Compiler returned: 1
核心问题是registry_t无法从unique<I>参数推导出模板参数T,编译器没办法逆向从ID找到对应的类型。而且我测试下来这个方案和MSVC也不兼容。
尝试方案3:ADL与友元重载
在2025年7月23日晚上7点左右,我又尝试了用参数依赖查找(ADL)结合友元重载的方式来实现,但目前还在摸索这个方向的具体实现细节。
内容来源于stack exchange
相关产品推荐
相关产品推荐

