从c++11开始,模板可以接受一组数量可变的参数,这种技术称为变参模板。
变参模板
下面一个例子,通过变参模板打印一组数量和类型都不确定的参数。
#include <iostream>
#include <string>
void print(void)
{
std::cout<<"........................"<<std::endl;
}
template <typename T, typename ... Ts>
void print(T arg1, Ts ... args)
{
std::cout<<arg1<<std::endl;
print(args ...);
}
int main(int argc, char **argv)
{
print("hello", 7.5, 10, std::string("building"));
}
看到上面这段代码,首先会产生两个疑问:
- void print(T arg1, Ts ... args)中arg1是什么作用?
- void print(void)有什么作用?
下面将通过解释这段代码的运行过程,来解答上面的问题。仔细观察上面这段代码,不难发现,print函数模板是一个递归函数模板。执行过程大体如下:
- main函数调用print("hello", 7.5, 10, std::string("building"));,"hello"赋值给arg1,其余参数赋值给args
- print函数输出"hello",再次调用自身print(7.5, 10, std::string("building"));,7.5赋值给arg1,其余参数赋值给args
- print函数输出7.5,再次调用自身print(10, std::string("building"));,10赋值给arg1,其余参数赋值给args
- print函数输出10,再次调用自身print(std::string("building"));,"building"赋值给arg1,args为空
- print函数输出"building",因为args为空,此时不在调用自身,而是重载函数print(void),然后结束递归。
从整个过程来看,arg1的主要作用就是从args迭代取值,print(void)负责处理args为空的情况。那么不定义void print(void)是否可以呢?答案是否定的,不定义该函数,编译将会报错“No matching function for call to 'print'”。
此处还应该注意一个问题,print和c/c++的printf原理不一样:printf通过va_list实现变参,而print函数模板是为每种情况都生成了一个重载函数,如下:
上面的信息来自于xcode调试,当然,也可以通过objdump查看,也会得到相同的结果,编译器确实生成了多个print重载函数:
当然,上面的代码还可以写成下面的样子:
template <typename T>
void print(T arg)
{
std::cout<<arg<<std::endl;
}
template <typename T, typename ... Ts>
void print(T arg1, Ts ... args)
{
print(arg1);
print(args ...);
}
如果代码中没有print(arg1),程序知会打印最后一个参数building,print只有迭代到最后一个参数时,才会找到合适的函数print(T arg)。
但一定要注意,下面的实现方式是错误的,无递归结束条件,无限迭代,直到耗尽堆栈空间:
void print(void)
{
}
template <typename ... Ts>
void print(Ts ... args)
{
print(args ...);
}
折叠表达式
从c++17开始,c++引入了一种更为简洁灵活的编程方式——折叠表达式,下面是一个简单的例子:
#include <cstdio>
template <typename ...T>
auto sum(T ... args)
{
return (... + args);
}
int main(int argc, char **argv)
{
int s = sum(1, 2, 3, 4, 5);
printf("%d\n", s);
}
几乎所有的二元运算符都可以用于折叠表达式,下面是一些其他运算符的例子:
template <typename F, typename ...T>
auto apply(F f, T ...args)
{
return (f(args), ...);
}
template <typename ...T>
bool and_op(T ...args)
{
return (args && ...);
}
迭代表达式,仅仅是围绕一个操作符简单地展开,例如连加。因此,对于三元操作符:?,很难用迭代表达式来实现。所以,想使用迭代表达式和:?求一个集合中的极值,是无法实现的。但是可以通过其他方式实现,下面便是一种实现方式:
template <typename T>
struct min_op final
{
public:
min_op(T data) : is_first(true), min_data(data) {
}
T operator()(T rhs) {
if (is_first) {
is_first = false;
min_data = rhs;
return min_data;
}
min_data = min_data < rhs ? min_data : rhs;
return min_data;
}
private:
bool is_first;
T min_data;
};
template <typename T, typename ...Ts>
auto min(T arg, Ts ... args)
{
min_op<T> op(arg);
return (op(args), ...);
}
很明显,这种方式还不如直接使用for循环直接利索。
与之前的递归迭代方式相比,迭代表达式最大的优点是编译器没有为其生成过多的重载函数。迭代表达式与之前的优点:不使用vector,不会生成多个函数,缺点:解决元素较少的情况。
变参模板的优点:
- 可以支持不同的类型
- 可以不使用容器
- 直接访问元素,效率比较高
但其并不是完美无缺的,:
- 不适用元素较多的情况
- 使用递归迭代会生成大量的重载函数
变参类模板和变参表达式
变参表达式
函数参数包除了转发所有参数外,还可以做其他事,例如计算他们的值。
template <typename ... Ts>
void print_doubled(Ts ... args)
{
print((args + args) ...);
}
...
print_doubled(1, 2, 3, 4, 5, 6);
...
变参下标
作为另外一个例子,下面的函数通过一组变参下标来访问第一个参数中相应的元素:
template<typename T, typename ...IDS>
void print_elems(T a, IDS ...ids)
{
print(a[ids]...);
}
...
std::vector<int> v{1, 2, 3, 4, 5, 6};
print_elems(v, 1, 3, 5);
...
变参模板类
提到变参模板类,首先会想到std::tuple,该种技术使得不定义新类型的前提下,多值返回成为一种可能,提供了更加灵活的编程方式,例如:
template <typename T>
std::tuple<T, T, T, T> calc(T x, T y)
{
return std::make_tuple(x + y, x - y, x * y, x / y);
}
...
auto result = calc(10.0, 2.5);
...
变参基类
变参基类从不定数的基类派生出一个新的类,主要目的是代码复用,比普通写法更加方便,派生类无需引入基类头文件,但需要注意多继承陷阱。下面是一个简单的例子:
#include <cstdio>
struct fly_animal
{
void fly(void) { printf("flying !\n"); }
};
struct swim_animal
{
void swim(void) { printf("swiming !\n"); }
};
struct run_animal
{
void run(void) { printf("running !\n"); }
};
struct fish
{
//...
};
struct bird
{
//...
};
struct mammal
{
//....
};
template <typename ...Bases>
struct overloader : Bases...
{
//using Bases::operator()...;
};
int main(int argc, const char **argv)
{
using flyfish = overloader<fly_animal, swim_animal>;
flyfish ff;
ff.fly();
ff.swim();
using crocodile = overloader<run_animal, swim_animal>;
crocodile ccdl;
ccdl.run();
ccdl.swim();
using cat = overloader<mammal, run_animal>;
cat ct;
ct.run();
return 0;
}