暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

C++ rtti vs 宏 - 如何优雅的获取类型T的name或ID

purecpp 2021-03-29
595

1. RTTI的实现

  如何获取一个类型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的转换, 作为常规实现, 它具有以下特点:

  1. 它确实能够比较好的生成一个自带hash实现的std::type_index, 可以很好的用它作为key来索引类型相关的对象.

  2. 利用相同的方法也可以很容易的获取类型T的字符串名称.

  3. 在rtti开启的情况下, 功能直接可获取.

  但不难发现, 这种方法也存在以下问题:

  1. 首先是依赖rtti, 在一些rtti关闭的情况下, 对应机制就停摆了

  2. 很重要的一点, 这是一个runtime实现, 也就是说, 天生与constexpr无缘, 我们没有办法做任何善于compiler time的优化.

  3. std::type_index的实现一言难尽, 虽然你可以通过hash获取一个size_t类型的ID, 但对于这个值的稳定性和有效性都依赖于库底层的实现, 当一些特定的场合需要跨App进行ID标识的时候, 它基本就没啥用了, 你只能退回字符串名称的方式去处理相关逻辑了.

2. FUNCSIG系统宏的实现

  那么有没有一种实现是不依赖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::namedetail(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 实现了.

文章转载自purecpp,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论