C++ 生成式的编译期类型名
本文将介绍由本人实现的一个 C++ 跨编译器 编译期 类型名 解决方案
——生成式的编译期类型名。
该方法有三大优点
- 编译期
- 跨编译器(类名有可预测性, 不同编译器同名 )
- 可以对类型名做类型计算
这是前所未有的(大概)
注意,我这里说的是“跨编译器”的方案,编译期类型名早就有了,但问题就是不同编译器名字不一样
如有反例,还请告知
因为依赖于许多模板工具,所以我将其实现在了我的通用模板库 UTemplate 中
相关主要代码(还会用到诸如函数性质
FuncTaits
,类型列表
TypeList
等基础工具)
- 工具:模板字符串 TStr.h
- 核心:接口 Name.h ,细节 details/Name.inl
0. 概要
C++ 获取类型名的标准方法为
std::type_info::name()
,但这是
运行时
的。
因此有人用了
__FUNCTION__
这个宏获取类型名(将类型放到模板函数上,然后从
__FUNCTION__
中截取出相应类型名),解决了
编译期
类型名问题(详细可参考 ctti,
nameof
) 。
但问题并没有很好解决,因为这个名字是 依赖于编译器 的,MSVC、Clang、GCC 都不完全一样。
而本文给出了一个跨编译器的编译期类型名解决方案。即,根据 C++ 的组合机制(数组,指针,模板,const/volatile/reference,函数等)将名字
解构
,再利用
__FUNCTION__
获取
“核心”类型名
,从而生成出完整类名。
由于类型名是生成式的,所以可以对类型名进行类型计算(如去除 const 等),十分灵活
1. 示例
基础
复杂
类型计算
2. 实现思路
我们需要用一个编译期容器来存储字符串,仅仅使用
static constexpr std::string_view
是不够的,我们需要将字符串放到模板里。目前的解决方案如下图
比如TStr<char, 'U', 'b', 'p', 'a'>
表示"Ubpa"
可用简单的宏TSTR("Ubpa")
直接得到该类型的值
原理参考 C++ 如何把字符串字面量的内容映射到类型? - SuperSodaSea的回答
本人扩展了一下,使其支持constexpr std::string_view
,constexpr char(&)[N]
等,并提供了相关操作,如substr
,remove_prefix
等。
相应代码为 TSTR.h
接着为了得到一个类型的名字,首先我们判断类型是否是组合类型(数组,指针,模板,const/volatile/reference,函数),如果是就去掉该组合机制,接着再获取剩余类型名。
如const int
,首先判断出其为const
,则剩余类型为int
。此时该类型不再包含其他组合机制,其名为int32
,则完整名为const{int32}
当所有组合机制都去除后,可以利用
__FUNCTION__
+ 模板函数的技术得到类型名。
示例
可以看到该模板函数的名字里包含了我们想要的类型名的“核心”部分
array
,只需要“裁剪”就可以得到。
类型名的核心部分是固定的/跨编译器的,任何编译器都能获得。然后又因为生成方式是固定的,因此类型的完整名字也是跨编译器的。
完美解决问题!
3. 类型解构
我们的核心步骤是类型解构,逻辑架构如下
看似简单,但实际上问题多多
下边我将分别介绍各种组合机制与相应的解构方法
3.1 基本组合机制
简单的组合机制是
const
,
volatile
,引用,指针,成员指针,方法如下
3.2 数组
稍微复杂点的是数组类型,如
int[][2][3]
,我们可以用
std::is_array
(是否为数组),
std::rank
(维数),
std::extent
(特定维的长度)
std::remove_extent
来解构。
3.3 函数
函数比较难,首先我们可以用
std::is_function
来判断其是否为函数类型。
接着我们要能解析一个函数类型,获取其各属性,如下
- 返回值类型
- 参数列表类型
- const/volatile/reference/noexcept
其中第三项总共有 2x2x3x2 = 24 种情况(
const
?
volatile
?
&/&&
?
noexcept
?)
const
和&/&&
来源于成员函数,如int MyClass::Func() const&
解析一个函数类型需要用到较多的模板编程的技法,这里直接展示工具(隐藏实现细节)
参数列表用
TypeList
打包起来,类型名的组合机制为
(参数 0 类型名, 参数 1 类型名, ..., 参数 n 类型名)-{const volatile &/&& noexcept}->{返回值类型名}
下图简单展示下其中几种情形,其余情形以此类推即可
3.4 模板
这部分十分具有技巧性
这部分我们的方法是将名字拆解成如下形式
核心类型名<模板参数 0 类型名, ..., 模板参数 n 类型名>
3.4.1 基本情形
我们可以利用模板偏特化解构出模板参数,如下
3.4.2 非类型模板参数
但问题是模板参数并不一定是类型,还可能是值,如
std::array<typename T, size_t N>
,这种类型没法用上边的模板偏特化搞定。
我的解决方案是将包含非类型模板参数的类转化成模板参数全为类型的代理类,原本的非类型模板参数用类型
std::integral_constant<typename T, T value>
包装起来。
如std::array<float, 5>
转化成proxy_type<float, wrapper_type_of<5>>
这种转换 一定程度上可以自动生成 ,下图就看看意思即可(我穷举了三个模板参数的情形,这样标准库里的模板类是基本覆盖了,用户如有需要可以自行扩展更多模板参数的情形)
3.5 核心类型名
这一步骤就是简单的裁剪工作,具体如下
- 从模板函数名中确定出类型名的所在段
-
移除
struct/class/enum
(MSVC 才有) - 删除核心名字后边的模板参数部分
- 删除核心名字前边的命名空间部分
3.6 常量名
3.4.2 提到了非类型模板参数,我们通过代理、包装、自动转换的方式基本解决了解构问题。其中非类型模板参数的名字还需要解决。
整数常量还是简单的,如
32
的名字是
"32"
。
比较偏的情况是成员指针(没想到吧,这东西也能放到模板参数里!),其名字解构成
类型名::&成员名
,其中
成员名
需要用户额外提供,如下
该部分结合静态反射库就能轻易完成
3.7 命名空间
之前提到,核心名是把命名空间给去掉了,因为这部分在某些情况下会比较糟糕
比如,类型
D
是模板类型
C
内定义的类型
那么
D
的命名空间里就包含了
C
,而这部分各家编译器给出的结果是不同的。我设计成用户可自行提供命名空间,否则直接用编译器提供的结果。
对比如下(MSVC)
使用 MSVC 提供的命名空间:C<class std::array<int,5> >::D
用户自定义的命名空间: C<std::array<int32,5>>::D
4. 类型计算
我们可以像操作类型那样操作类型名,也即可以做类型计算
如
const int
,它的类型名为
const{int32}
,那么如果想要实现类似
std::remove_const
的计算,可以对上述类型名进行裁剪(去除前缀
const{
和后缀
}
)。
示例
5. 总结
文章到这里就结束了。
总的来说,本文解决的是 C++ 语言标准上的问题。
C++ 纯属恶心人。
虽然原理部分我说了一大堆,该功能用起来还是很简单的。