介绍一些基本技巧,本文是很多博客和书籍的学习笔记,各部分内容可能会有少许重复
其中的代码我虽然已经写了很多遍了,但是让我不看资料……我还真写不出来
权当自己存的编译期代码库吧
越来越感觉自己代码量的不足,这么菜还怎么看chromium里10个G的C++代码
元函数与type_traits
类型元函数
考虑如下情形:我们希望进行类型映射F,F(int)=unsigned int,F(long)=unsigned long
这种映射可以看成函数,只不过输入和输出都是类型而已
1 | template<typename T> //输入(模板参数) |
Fun不是一个标准的元函数,因为它没有内嵌类型type。但是它具有输入(T),输出(Fun
),也明确定义了映射规则,因此可以视作元函数。
事实上,代码中展示的也是C++标准库中定义元函数的常用方式,比如std::enable_if
和std::enable_if_t
,前者就像Fun_一样是内嵌了type类型的元函数;后者像Fun一样是基于前者给出的定义,用于简化使用。
命名方式
此处约定如下:如果函数返回值需要用某种依赖型的名称表示,那么函数被命名为XXX_的形式(以下划线为后缀);反之非依赖型则不包含下划线
1 | template<int a,int b> |
type_traits
type_traits是由boost库引入的,由C++11纳入其中,头文件
1 |
|
更多的一些用于编译期判断的函数
1 |
|
还有一个朽化(std::decay)操作
为类型T应用从左值到右值(lvalue-to-rvalue)、数组到指针(array-to-pointer)和函数到指针(function-to-pointer)的隐式转换。转换将移除类型T的cv限定符(const和volatile限定符),并定义结果类型为成员decay
::type的类型。
1 | typedef std::decay<int>::type A; // int |
朽化还可以将函数类型转换成函数指针类型,从而将函数指针变量保存起来,以便在后面延迟执行
using FnType = typename std::decay
::type;实现函数指针类型的定义
(暂时不太理解,可能与惰性求值有关)
编译期类型推导
auto
NULL
decltype
RTTI机制为每一个类型产生一个type_info类型的数据,而typeid查询返回的变量相应type_info数据,通过name成员函数返回类型的名称,同时C++11中typeid还提供了hash_code这个成员函数用于返回类型的唯一哈希值
泛型编程中我们需要的就是编译时确定类型,RTTI无法满足这样的要求;
而编译时类型推导,除了auto,还有就是decltype
decltype并不是像auto一样从变量声明的初始化表达式获得类型,而是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值
推导表达式类型
1 | int i = 4; |
定义类型
与using/typedef合用,用于定义类型
1 | using size_t = decltype(sizeof(0)); // 返回值为size_t类型 |
重用匿名类型
举个例子:重新使用匿名结构体
1 | struct { |
追踪函数返回类型
结合auto,这也是decltype最大的用途
1 | template <typename _Tx, typename _Ty> |
结合右值引用和完美转发,更能体现这一点
1 | template <class Arg, class F> |
模板型模板参数&容器模板
C++元函数可以操作的数据包含3类:数值、类型与模板,统一被称作元数据,以示与运行期所操作的数据的区别。
模板作为元函数的输入
1 |
|
其中元函数Fun接收两个参数:一个模板和一个类型
从函数式编程的角度来说,Fun是一个典型的高阶函数,即以另一个函数为输入参数的函数
总结为数学表达式 $Fun(T_1,t_2)=T_1(t_2)$
而模板作为元函数的输出相关内容在实际中使用较少,就不介绍了(其实是我不会)
容器模板
我们需要的是一个容器:用来保存数组中的每个元素,元素可以是数值、类型或模板。典型的容器仅能保存相同类型的数据,但已经可以满足绝大多数的使用需求。
C++11中引入了变长参数模板(variadic template),借由此实现我们的容器
1 | template<int... Vals> struct IntContainer; |
这些语句实际上只是声明而非定义,事实上这是一个元编程中的一个惯用法:仅在必要时才引入定义,其他的时候直接使用声明即可。
关于变长参数模板后文还会提到
顺序、分支与循环
顺序执行
1 |
|
代码中先根据T计算出inter_type,再用这个中间结果计算出type
结构体中的所有声明都要看成执行的语句,不能更换顺序
这里改变顺序确实会报错,类似地,在下文 分支选择与短路逻辑 中有一些想法
在编译期,编译器会扫描两遍结构体中的代码,第一遍处理声明,第二遍才会深入到函数定义中。
如果先扫描到type,发现它依赖于未定义的inter_type,就不会继续扫描而是报错。
分支执行
std::conditional(_t)
conditional和conditional_t为type_traits中提供的2个元函数,定义如下:
1 | namespace std { |
注意这里只偏特化了false的情况,但是却可以完美的表达true的情形。
我个人猜测与编译器最佳匹配的实现有关,后面也会提到SFINAE
逻辑行为:如果B为真,则函数返回T,否则返回F
1 | //测试代码 |
使用特化实现分支
特化天生就是用于引入差异的,因此可以用于实现分支
1 | struct A; struct B; |
Fun_元函数实际上引入了3个分支,分别对应输入参数为A、B与默认情况
这里A和B只是用于特化的类型,因此只需要声明,不需要定义
可能与下一章异类词典中用类名作为键类似
C++14中另一种特化方式:
1 |
|
这段代码里特化的2处模板,vs报constexpr在此处无效,不清楚为什么
必须在其首次使用之前对 变量 “Fun [其中 T=A]” 进行显式专用化()
书上提醒:在非完全特化的类模板中引入完全特化的分支代码是非法的
注释代码g++编译失败,vs成功
1 |
|
改进后的代码引入了一个伪参数TDummy,用于将原有的完全特化转换为部分特化
但是设定了默认值void,因此可以直接以Fun_<int>
调用这个元函数,无需赋值
使用std::enable_if(_t)
1 | //代码原型 |
这里T不是特别重要,重要的是当B为true时,enable_if元函数可以返回结果type,可以基于此构造实现分支
代码实例
1 | template<bool IsFeedbackOut, typename T, |
这里引入分支,根据IsFeedback的值来匹配模板
SFINAE
SFINAE(Substitution Failure Is Not An Error),译为”匹配失败并非错误”
当匹配模板时,编译器即使已经匹配到了一个足够好的选择,也会把所有选择都尝试匹配,最后选择最佳的
这里std::enable_if(_t)正是利用了这一点
有些情况下,我们希望引入重名函数,它们无法通过参数类型加以区分,这时enable_if(_t)能在一定程度上解决相应的重载问题
补充
std::enable_if(_t)也有一些缺点,并不像模板特化那么直观,可读性较差
这里给出的代码实例是一个典型的编译期与运行期结合的使用方式,FeedbackOut_中包含了运行期的逻辑,而选择哪个FeedbackOut_则是通过编译期的分支来实现
计算实例
利用二分法计算整数的平方根,结果向下取整
1 |
|
编译期分支与多返回类型
1 |
|
这是一种典型的编译期分支和运行期函数结合的例子,C++17引入了if constexpr
来简化代码
1 |
|
其中if constexpr
必须接收一个常量表达式
循环执行
为了更有效地操纵元数据,往往选择递归的形式来实现循环
举个例子:给定一个无符号数,求该整数所对应的二进制表示中1的个数
1 |
|
循环使用更多的一类情况则是处理数组,以下给出一个实例
1 | template<size_t...Inputs> |
当输入数组为空时,会匹配第一个模板特化,返回0;如果有正数个参数,则匹配第二个模板特化
以下使用C++17中的折叠表达式(fold expression)简化循环
1 | template<size_t... values> |
小心实例化爆炸与编译崩溃
编译时实例化的模板会被保存起来用以可能的复用,对于一般的C++程序,可以极大地提升编译速度;但是对模板元编程来说,很可能造成灾难,考虑以下代码:
计算 $\sum_{i=1}^{A+ID}i$
1 |
|
循环所产生的全部实例都会在编译器中保存,如果有大量实例,很可能会内存超限甚至崩溃
解决方案:把循环拆分出来以复用
1 |
|
但是也有一些不足之处:之前的代码imp被置于Wrap_中,体现二者的紧密联系,从名称污染的角度来说,这样做不会让imp污染Wrap_外围的名字空间
后一种实现中,imp将对名字空间造成污染:在相同的名字空间中,我们无法再引入一个名为imp的构造,以供其他元函数使用
权衡:如果元函数的逻辑比较简单,同时不会产生大量实例,那么保留前一种(对编译器来说比较糟糕的形式);反之,如果元函数逻辑比较复杂(典型情况是多重循环嵌套),又可能产生很多实例,就选择后一种以节省编译资源。
当然,选择后一种方式时,我们可以引入专用的名字空间来尽力避免名称污染
分支选择与短路逻辑
计算实例:计算1~N是否全为奇数
1 |
|
但这种逻辑短路的行为在上述程序中并没有得到利用——无论is_cur_odd是什么,AllOdd_都会对is_pre_odd进行求值,改进如下:
1 |
|
奇特的递归模板式
奇特的递归模板式(Cruiously Recurring Template Pattern,CRTP)是一种派生类的声明方式
奇特之处在于:派生类会将其本身作为模板参数传递给其基类
1 | template<typename D>class Base{ /*...*/ }; |
似乎看上去有循环定义的嫌疑,但它确实是合法的…
静态多态
给出两个方面的例子
例1
1 |
|
注意这里使用的是static_cast
而不是dynamic_cast
,因为只有继承了Base的类型才能调用interface
,而且这里是向下转型,所以这样的行为是安全的
这样既实现了虚函数的效果,又没有虚函数调用时的开销,同时类的体积相比使用虚函数也会减少(不需要存储虚表指针),但是缺点是无法动态绑定
例2
1 |
|
Animal中的Run是通过类型转换后调用模板类型的Run方法实现的
在Action模板函数中接收Animal类型的引用(或指针)并在其中调用了animal对象的Run方法,由于这里传入的是不同的子类对象,因此Action中的animal也会有不同的行为
添加方法,减少冗余
假设我们现在需要实现一个数学运算库,支持Vector2、Vector3等等,如果我们将每个类分别声明并实现如下
1 | //Vec3 |
我们会发现需要为每个类型都实现+=, -= ,++ , — , + , -等运算符重载,而且每个类型的一些运算符,行为都很类似,而且可以使用其他的运算符进行实现,比如+=, -=, ++, —都可以采用+,-运算符进行实现。这时我们就可以采用CRTP抽离出这些共同的类似方法,减少代码的冗余:
1 | template<typename T> |
通过把+=, -=等操作放到基类中并采用+ ,-运算符实现,这样一来所有继承自VectorBase的类,只要其定义了+,-运算符就可以自动获得+=, -=等运算符,减少了代码中的冗余。
在有多个类型存在相同方法,且这些方法可以借助于类的其他方法进行实现时,均可以采用CRTP进行精简代码。
模板中的变长参数
可变模板参数函数
基本形式
1 | template<typename... Args> //模板参数包,包含函数调用中的参数匹配的类型,比如char*,int |
获取参数包信息
计算参数个数
一个实例
1 |
|
获得每个参数
递归
递归方法需要一个终止函数
1 |
|
求和
1 |
|
逗号表达式
实例如下
1 |
|
C/C++中的表达式会按顺序执行,同时这里用到了C++11的特性——初始化列表
(printarg(args),0)
会先执行函数,再得到逗号表达式的结果0
通过初始化列表来初始化一个变长数组
{(printarg(args),0)...}
将会展开成((printarg(arg1),0),(printarg(arg2),0),etc...)
最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]
这里只用于展开参数包,我们可以将函数作为参数,就可以支持lambda表达式了
可变模板参数类
比较基本的就是这个tuple了…
1 | std::tuple<int> tp1 = std::make_tuple(1); |
由于可变参数模板的模板参数个数(绕口令?…)可以为0,所以以下定义也是合法的
std::tuple<> tp;
展开参数包的方法
模板偏特化和递归
(感觉和刚刚的求和没什么区别,换些实例)
实例1:IntegerMax
1 |
|
这段代码看起来比较复杂……实际上是一个继承,integral_constant<size_t,num>
应该就是size_t num
实例2:MaxAlign
上一段代码改一下可以轻松实现获取最大内存对齐值的元函数MaxAlign
增加以下部分
1 | template<typename... Args> |
实例3:TypeSizeSum
我自己写的也不太熟练,还是再来一个求和吧,计算参数包中参数类型的size之和
突然想用LaTeX写Σ了…
1 |
|
继承方式
MakeIndexes的作用是生成一个可变参数模板类的整数序列,最终输出的类型是:struct IndexSeq<0,1,2>0,1,2>
(看懂代码已属不易,这个类型的实际使用我还没有考虑)
1 |
|