上文我们简单地介绍了变长模板参数。对于数量不确定的模板参数,通常的做法是通过递归的方式一个一个地处理每个参数,直到所有参数处理完毕。这个理解起来比较容易,但实现稍显麻烦。C++17引入了折叠表达式(fold expression)来简化对模板参数包的处理。
我们重新定义一下sum,来看看折叠表达式的威力。
template<typename... Types>
auto sum(Types... args) {
return (... + args);
}
sum(3, 6.5, 35, 7); // 51.5
干净利索,这才是代码该有的样子。注意,...在操作符+的左边,称为左折叠,运算顺序为(((3+6.5)+35)+7)。有左当然有右了。
template<typename... Types>
auto sum(Types... args) {
return (args + ...);
}
这回的运算顺序为(3+(6.5+(35+7)))。在这里无论是左还是右,对结果本身并无差别,当然其他情况就令当别论了。
对了,还记得上文我们曾经说过变长的模板参数可以是0个参数吗?那我们调用sum()试试看。哦,编译出错了。对于空的变长模板参数,编译器会给它赋一个回退值(fallback value),让它还是能够正确参与表达式的运算。这里的问题是+需要两个操作数,但是我们只有一个回退值。怎么办呢?我们可以增加一个初始值来解决这个问题。
template<typename... Types>
auto sum(Types... args) {
return (0 + ... + args); // (args + ... + 0);
}
折叠除了简化对变长参数列表中的元素操作于二元运算符之外,还可以应用于很多场合,如函数调用,初始化列表等等。我们看下面的例子,把若干参数逐一输出,中间用空格分隔,这个是个常见的任务。
template <typename T>
const T &spaceBefore(const T &arg) {
std::cout << ' ';
return arg;
}
template <typename First, typename... Args>
void print(const First &firstarg, const Args &...args) {
std::cout << firstarg;
(std::cout << ... << spaceBefore(args)) << '\n'; // std::cout << spaceBefore(arg1) << spaceBefore(arg2) << ...
}
下面看个更酷的例子
template <typename... Bases>
class MultiBase : private Bases... {
public:
void print() { (..., Bases::print()); }
};
让某个类继承自若干个基类,print()将逐一调用每个基类的同名函数。C++模板的短小精悍而又威力强大可见一斑(用尽量少的代码构建强大的表达能力,无论对可读性、可维护性都是一个巨大的提升)。
我们最后看一个比较实际的例子。大家都知道lambda表达式实际上是个定义了operator()的匿名类,因为不知道类名,无法将其作为基类,利用模板这个问题就轻而易举了。variant是C++17引入的一个类模板,类似于C中的union,在其内部允许放置不同类型的对象,但同时只能存在一个。
template <typename... Ts>
struct overload : Ts... {
using Ts::operator()...;
};
template <typename... Ts>
overload(Ts...) -> overload<Ts...>;
我们定义了一个类模板overload,它会把所有基类的operator()定义都引入到自己的定义中。正好借这个机会我们再复习一下类模板参数的推断。由于overload只有缺省的构造函数,所以无法从中完成参数推断,而且我们的目的是从多个lambda表达式继承,也不方便显式给出它们的实际类型,定义一个推断向导恰逢其时。
std::variant<int, std::string> var(42);
auto twice = overload{
[](std::string &s) { s += s; },
[](auto &i) { i *= 2; },
};
std::visit(twice, var);
功能很简单,调用时会将对象内容加倍。visit()时会根据对象中存放的实际类型自动调用参数类型匹配的那个版本的lambda。