C++ 20 编译期类型名获取
编译期类型名获取
C++20 标准,使用库 std::source_location
。
#include <source_location>
C++ 20 之前
在 C++ 20 前有两种方法
__PRETTY_FUNCTION__
__FUNCSIG__
通过截取函数签名中的 T = ...
获取函数类型。
template <typename T> constexpr auto type_name() -> std::string_view { using std::literals::string_view_literals::operator""sv; constexpr auto prefix = "T = "sv; constexpr auto suffix = ";]"sv; constexpr std::string_view detail = __PRETTY_FUNCTION__; constexpr auto pre_rng = std::ranges::search(detail, prefix); static_assert(!pre_rng.empty()); constexpr std::ranges::subrange subrange {pre_rng.end(), detail.end()}; constexpr auto suf = std::ranges::find_first_of(subrange, suffix); static_assert(suf != detail.end()); return {pre_rng.end(), std::distance(pre_rng.end(), suf)}; }
缺点:两者都不是标准宏,且 __PRETTY_FUNCTION__
为 GCC 拓展。而且宏也越来越被标准库中的函数代替,比如本文就是使用 std::source_location
库中的 function_name()
代替 __PRETTY_FUNCTION__
。
还有两个个作用相似的宏 __FUNCTION__
、__func__
(C99 标准),不过它们只能获取函数名,没有模版中的类型。实际上这些宏都是由编译器隐式定义在每个函数中的(只读)变量。
std::type_info
abi::__cxa_demangle
template <typename T> auto type_name() -> std::string { std::type_info const& t = typeid(T); char const* name = t.name(); int status; std::unique_ptr<char, void (*)(void*)> res { abi::__cxa_demangle(name, NULL, NULL, &status), std::free }; return status == 0 ? res.get() : name; }
其中 typeid(T).name()
返回的是运行时获取的类型签名, abi::__cxa_demangle
是 GCC 提供的将类型签名转换为类型名的函数(非 GCC 就没有这个函数了)。
typeid(T).name()
获得的类型签名不一定等于类型名,比如 typeid(std::string).name()
得到的签名就可能是 NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
,所以需要 abi::__cxa_demangle
进行转换。
缺点:非编译期,且 cppreference
提到 std::type_info::name()
并不提供保证类型签名是唯一的,因此具有一定的不确定性。
(
std::type_info::name
)Returns an implementation defined null-terminated character string containing the name of the type. No guarantees are given; in particular, the returned string can be identical for several types and change between invocations of the same program.
std::source_location
使用 std::source_location::current().function_name()
可以获取这个语句所在的函数的函数签名。实际上是取代了 __PRETTY_FUNCTION__
。可以在 编译器支持 表中查看对不同编译器对 source_location
的支持,现在主流的编译器(GCC11、Clang16)都已经支持了。
std::source_location::function_name()
为 constexpr
函数,因此可以在编译期就获取到函数名称。
更多信息请查看 cppreference,其中还有函数 line()
column()
file_name()
等可以代替 __LINE__
__FILE__
等宏。
编译期顺序
特别要注意编译期的顺序。
比较下面两个函数:
template <typename T, typename U> consteval auto foo() -> std::string_view { constexpr auto p = std::source_location::current().function_name(); return p; } template <typename T, typename U> consteval auto fun() -> std::string_view { return std::source_location::current().function_name(); }
如果编译器在每次编译期将 foo()
完全重新计算一遍 p
,那么 foo()
和 fun()
的结果是相同的;如果编译器只将 p
计算一次,此时每次调用 foo()
得到的结果都是第一次的 p
。
不同编译器对在编译期计算顺序是不同的。可以在 foo()
中去掉声明 p
时的 constexpr
。
template <typename T, typename U> consteval auto foo() -> std::string_view { auto p = std::source_location::current().function_name(); return p; }
此时 foo()
和 fun()
的结果相同。
实战:类型实例表
get<T>()
从表中获取T
的实例;put<T>(Args...)
在表中放置T
的实例。
任何想要转换为 std::any
的类型要具有拷贝构造函数 T(T const&)
,为了避免这一点,填入表中的实例是类型 std::shared_ptr<T>
而不是 T
,因为 std::shared_ptr
总是可以拷贝的。
#include <source_location> #include <any> #include <map> #include <memory> #include <string_view>
- 禁止
const
类型和reference
类型,也就是说get<T&>
get<T const>
之类的无法通过编译,我们需要的是纯粹的类型。使用concept
设置这个限制。
template <typename T> concept is_not_cr = !std::is_const_v<T> && !std::is_reference_v<T>;
在 template
中用 is_not_cr
声明类型即可应用这一限制,比如使用 name_detail()
转发 function_name()
函数的实现。再添加 consteval
强制在编译期求值。
template <is_not_cr T> struct Helper { consteval auto operator()() const -> std::string_view { return std::source_location::current().function_name(); } };
GCC 的实现会用 <function> [with <template>; ...]
,且类型信息更丰富,会带有函数限制符号 constexpr
等。Clang 的实现会用 <function> [<template>; ...]
。其中 <template>
为 T = ...
,即模版中代表类型的名称。总之在不同的编译器实现中都要定位 T = ...
这一句。
使用 std::ranges
中的函数(都是 constexpr
函数),它们都需要接受 range
类的参数,也就是说 char const*
不行(只有开头,没有结尾、长度),需要 std::string_view
类型。
template <is_not_cr T> static consteval auto type_name() noexcept -> std::string_view { using std::literals::string_view_literals::operator""sv; constexpr auto prefix = "T = "sv; constexpr auto suffix = ",;]"sv; constexpr auto detail = Helper<T> {}(); constexpr auto pre_rng = std::ranges::search(detail, prefix); static_assert(!pre_rng.empty()); constexpr std::ranges::subrange subrange {pre_rng.end(), detail.end()}; constexpr auto suf = std::ranges::find_first_of(subrange, suffix); static_assert(suf != detail.end()); return {pre_rng.end(), std::distance(pre_rng.end(), suf)}; }
解决了类型名称获取的问题,现在就是使用 std::map
存放其与对应类型实例的映射了。
使用 std::any
来存储任意类型(其实是 C 语言中 void*
的代替),通过 std::any_cast
进行类型转换。
语义上,std::optional<T&>
应该更符合 get()
的返回值,但不存在这种类型。所以使用 std::shared_ptr<T>
来返回。或者你也可以选择在没有对应的键的情况下直接抛出错误。
Args&&
万能引用和std::forward
转发,令参数在传入put()
时和传给new T()
时保持参数的引用类型和const
限制。put()
返回可能的本来存在于map
中的实例。
template <is_not_cr T> static auto get() -> ptr<T> { if (auto find = map.find(type_name<T>()); find != map.end()) { return std::any_cast<ptr<T>>(find->second); } return {}; } template <is_not_cr T, typename... Args> static auto put(Args&&... args) -> ptr<T> { ptr<T> p {new T(std::forward<Args>(args)...)}; auto [it, b] = map.try_emplace(type_name<T>(), std::move(p)); if (b) return {}; p.swap(std::any_cast<decltype((p))>(it->second)); return p; }
全部代码如下
#include <any> #include <map> #include <memory> #include <source_location> #include <string_view> template <typename T> concept is_not_cr = !std::is_const_v<T> && !std::is_reference_v<T>; class Instance { Instance() = delete; template <is_not_cr T> using ptr = std::shared_ptr<T>; template <is_not_cr T> struct Helper { consteval auto operator()() const -> std::string_view { return std::source_location::current().function_name(); } }; static std::map<std::string_view, std::any> map; public: template <is_not_cr T> static consteval auto type_name() noexcept -> std::string_view { using std::literals::string_view_literals::operator""sv; constexpr auto prefix = "T = "sv; constexpr auto suffix = ",;]"sv; constexpr auto detail = Helper<T> {}(); constexpr auto pre_rng = std::ranges::search(detail, prefix); static_assert(!pre_rng.empty()); constexpr std::ranges::subrange subrange { pre_rng.end(), detail.end() }; constexpr auto suf = std::ranges::find_first_of(subrange, suffix); static_assert(suf != detail.end()); return {pre_rng.end(), std::distance(pre_rng.end(), suf)}; } template <is_not_cr T> static auto get() -> ptr<T> { if (auto find = map.find(type_name<T>()); find != map.end()) { return std::any_cast<ptr<T>>(find->second); } return {}; } template <is_not_cr T, typename... Args> static ptr<T> put(Args&&... args) { ptr<T> p {new T(std::forward<Args>(args)...)}; auto [it, b] = map.try_emplace(type_name<T>(), p); if (b) return {}; p.swap(std::any_cast<decltype((p))>(it->second)); return p; } }; std::map<std::string_view, std::any> Instance::map;
本文作者:violeshnv
本文链接:https://www.cnblogs.com/violeshnv/p/17844993.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步