如何获取一个类型T的name或者唯一ID, 对于这个问题, 最常规的方式应该是借助C++的rtti了, 比如如ponder中所使用的方式:
namespace ponder {
namespace detail {
typedef std::type_index TypeId; // Used to uniquely identify a type.
// Calculate TypeId for T
template <typename T>
inline TypeId calcTypeId() {return TypeId(typeid(T)); }
}
}
其中的TypeID其实就是std::type_index, ponder中大量依赖这种方式进行 T -> std::type_index的转换, 作为常规实现, 它具有以下特点:
但不难发现, 这种方法也存在以下问题:
那么有没有一种实现是不依赖rtti, 又能很好的适配constexpr的呢? 答案是肯定的. 比如decs中对类型T的hash()方式:
struct MetatypeHash {
size_t name_hash{ 0 };
size_t matcher_hash{ 0 };
bool operator==(const MetatypeHash& other)const {
return name_hash == other.name_hash;
}
template<typename T>
static constexpr const char* name_detail() {
return __FUNCSIG__;
}
template<typename T>
static constexpr size_t hash() {
static_assert(!std::is_reference_v<T>, "dont send references to hash");
static_assert(!std::is_const_v<T>, "dont send const to hash");
return hash_fnv1a(name_detail<T>());
}
};
如代码所示, 很好的解决了compiler time生成name_detail()和hash()的问题, hash这个地方使用了一个hash_fnv1a()的算法, 不是本文关注的重点, 这里不展开了. 通过constexpr, 很好的解决了编译期获取类型T的唯一字符名称和64位hash的问题, 而且不依赖rtti, 比较优雅的解决了T->name和T->id的问题.
当然, 上面的代码其实没有解决跨平台的问题, 另外对某一类型T, 获取到的其实是类似 "const char *__cdecl ecs::MetatypeHash::name_detail(void)"这样的值, 如果我们需要关注name, 以及需要跨平台, 那么显然以上的设施还不够, 但最核心的问题解决了, 其它的就比较简单了, 因为代码比较简单, 这里直接贴代码了(感谢黄老板@spiritsaway 的指导), 最后实现的代码如下:
struct MetatypeHash {
size_t name_hash{ 0 };
size_t matcher_hash{ 0 };
bool operator==(const MetatypeHash& other)const {
return name_hash == other.name_hash;
}
template<typename T>
static constexpr const char* name_detail() {
#if RSTUDIO_CORE_PLATFORM == RSTUDIO_CORE_PLATFORM_LINUX
return __PRETTY_FUNCTION__;
#else
return __FUNCSIG__;
#endif
}
#if RSTUDIO_CORE_PLATFORM != RSTUDIO_CORE_PLATFORM_WIN32
template<typename T>
static constexpr auto name_pretty()
{
//name_detail() is like: static constexpr const char* ecs::MetatypeHash::name_detail() [with T = rstudio::math::Vector3]
std::string_view pretty_name = name_detail<T>();
std::string_view prefix = "static constexpr const char* ecs::MetatypeHash::name_detail() [with T = ";
std::string_view suffix = "]";
pretty_name.remove_prefix(prefix.size());
pretty_name.remove_suffix(suffix.size());
return pretty_name;
}
#else
template<typename T>
static constexpr auto name_pretty()
{
//name_detail() is like "const char *__cdecl ecs::MetatypeHash::name_detail<class rstudio::math::Vector3>(void)"
std::string_view pretty_name = name_detail<T>();
std::string_view prefix = "const char *__cdecl ecs::MetatypeHash::name_detail<";
std::string_view suffix = ">(void)";
pretty_name.remove_prefix(prefix.size());
pretty_name.remove_suffix(suffix.size());
size_t start_of = pretty_name.find_first_of(' ') + 1;
return pretty_name.substr(start_of);
}
#endif
template<typename T>
static constexpr size_t hash() {
static_assert(!std::is_reference_v<T>, "dont send references to hash");
static_assert(!std::is_const_v<T>, "dont send const to hash");
return hash_fnv1a(name_detail<T>());
}
};
这样一些不需要友好名字的地方我们可以使用name_detail(), 一些需要友好名字的地方, 我们使用name_pretty(), 至于hash(), 看自己的使用场合, 可以灵活选用name_detail()或者name_pretty()了, 以上实现在VS2019和GCC8.3上测试过, clang的话需要处理一下, 宏跟Linux下一样都是 PRETTY_FUNCTION, 但前缀会稍有区别, 对应的name_pretty()实现需要做下调整, 手边没有安装Clang的机器, 就先不贴出适配Clang的版本了.
这种compiler time的优化对于支持C++17特性的编译器来说, 还是值得去做的, 一些高频操作, 参与Deduce的类型又比较可预估的时候, 我们肯定会倾向于用更多的compiler time实现来取代像前文提到的rtti这种runtime 实现了.