现代C++之”可变参数模板”
从17年开始就不太做C++开发了,因此知识还停留在C++03上,所以这次趁着过年的短暂空隙补一下比较有意思的现代C++特性,有些简单的新语法不是我想表达的重点就一笔带过了。
本文通过1个简单例子来了解”可变参数模板”,代码:https://github.com/owenliang/modern-cpp/blob/main/variadic_tpl.cpp。
这是模板的一个新特性,我们从一个需求出发更容易解释:
1 2 3 |
int main(int argc, char** argv) { double s = sum(1, 1.1, 2.2, 3, 4.5); std::cout << s << std::endl; |
实现一个sum函数,可以接受任意长度+任意类型的传参,然后返回一个double类型的总和。
如果用C++03,那么类型一样还好说点,我可以重载N个不同参数个数的版本,但是现在类型也是不限制的,咋弄呐?那就看看现代C++的”可变参数模板吧”。
1 2 3 4 |
template<typename T, typename... Args> double sum(T v, Args... args) { return v + sum(args...); } |
typename… Args表示多个模板参数,每个模板参数的类型可以不一样。
然后sum函数接受T v,以及后续要求和的N个参数,Args…可以展开函数形参声明,args…可以展开实际参数,因此sum是一个递归函数。
sum(1, 2, 3)等于1 + sum(2,3)等于1+2+sum(3)等于1+2+3,回想模板与模板实例化的过程,因此编译期会为模板实例化3个版本的sum函数:
sum(int, int, int)
sum(int, int)
sum(int)
编译器甚至聪明到可以帮我们把这个运行时尾递归在编译期间展开,这个就不扯远了。
但是这还没算完,注意上面的sum函数至少接收1个T v参数(Args…可以容纳0个参数),但是递归最终会出现sum(3)等于3+sum()的情况,因此我们必须给这个递归一个出口,也就是重载一个空参数的版本:
1 2 3 4 5 6 7 8 |
double sum() { return 0; } template<typename T, typename... Args> double sum(T v, Args... args) { return v + sum(args...); } |
而且还得注意double sum()必须写在上面,因为下面的template函数实例化出来的sum(int)版本会调用sum(),因此需要sum()先出现,否则会报找不到sum()函数的错误,都是小细节啊。
另一个超酷的sum实现
动态模板参数就是靠上述递归的思想完成了多个模板实例化的生成,其核心就是递归过程中如果Args…列表消耗完了就不要再递归了,能不能不用重载方式实现递归出口呢?下面就来看一个厉害的操作:
1 2 3 4 5 6 7 8 |
template <typename T, typename... Args> double sum_super_cool(T v, Args... args) { if (sizeof...(args) == 0) { return v; } else { return v + sum_super_cool(args...); } } |
哈哈,sizeof…可以求”可变模板参数”的长度,注意这是编译期生成模板实例化代码过程中执行的,不是运行时的。
这个很容易理解的,sum_super_cool(1,2,3)会生成如下几个模板实例化版本:
sum_super_cool(int,int,int)
sum_super_cool(int,int)
sum_super_cool(int) — > 生成这个版本时,sizeof…(args) 就是0,所以该版本生成的代码实际就是:
1234567 double sum_super_cool(int v) {if (0 == 0) {return v;} else {return v + sum_super_cool();}}
也就是说sum_super_cool(int)在运行时会执行if(0==0)的判定然后就return v了,例如sum_super_cool(int, int)的if(1==0)就会导致走else分支继续递归调用v+sum_super_cool()。
但实际编译这个代码会失败,因为虽然sum_super_cool(int)会直接return v,但是return v+sum_super_cool()代码还是被生成出来的,这就要求我们必须实现double sum_super_cool()版本的重载才能编译通过:
1 2 3 4 5 6 7 8 9 10 11 12 |
double sum_super_cool() { return 0; } template <typename T, typename... Args> double sum_super_cool(T v, Args... args) { if (sizeof...(args) == 0) { return v; } else { return v + sum_super_cool(args...); } } |
好,我们再引入一个高级语法,让我们可以不用手动重载空参数版本代码的同时也能终止递归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
template <typename T, typename... Args> double sum_super_cool(T v, Args... args) { if constexpr (sizeof...(args) == 0) { return v; } else { return v + sum_super_cool(args...); } } int main(int argc, char** argv) { double ss = sum_super_cool(1, 1.1, 2.2, 3, 4.5); std::cout << ss << std::endl; return 0; } |
在if后面加constexpr,可以让编译器在编译阶段直接根据if (sizeof…) {} else {} 语句进行代码裁剪,如果是if (0==0),那么实例化的代码就是:
1 2 3 |
double sum_super_cool(int v) { return v; } |
否则就会被裁剪成这样(某一个版本的模板实例化代码):
1 2 3 |
double sum_super_cool(int v, int x, int y) { return v + sum_super_cool(x,y); } |
这样就不会出现调用sum_super_cool()空传参版本的重载需求啦~
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
