C++11 增加了一个新特性变参模板(variadic template),它可以接受任意个模版参数,参数包不能直接展开,需要通过一些特殊的方法,比如函数参数包的展开可以使用递归方式或逗号表达式,在使用的时候有点难度。
C++17 解决了这个问题,通过折叠表达式(fold expression)简化对参数包的展开。
折叠表达式共有四种语法形式,分别为一元的左折叠和右折叠,以及二元的左折叠和右折叠。
一元左折叠(unary left fold)
( ... op pack )
一元左折叠 (... op E) 展开之后变为 ((E1 op E2) op ...) op En
一元右折叠(unary right fold)
( pack op ... )
一元右折叠 (E op ...) 展开之后变为 E1 op (... op (En-1 op En))
二元左折叠(binary left fold)
( init op ... op pack )
二元左折叠 (I op ... op E) 展开之后变为 (((I op E1) op E2) op ...) op En
二元右折叠(binary right fold)
( pack op ... op init )
二元右折叠 (E op ... op I) 展开之后变为 E1 op (... op (EN−1 op (EN op I)))
(1)语法形式中的op代表运算符,pack代表参数包,init代表初始值。
(2)不指定初始值的为一元折叠表达式,而指定初始值的为二元折叠表达式。
(3)初始值在右边的为右折叠,展开之后从右边开始折叠。而初始值在左边的为左折叠,展开之后从左边开始折叠。
(4)当一元折叠表达式中的参数包为空时,只有三个运算符(&& || 以及逗号)有缺省值,其中&&的缺省值为true,||的缺省值为false,逗号的缺省值为void()。
fold expression支持32种操作符:
+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*
(1)一元右折叠
从表达式右边开始fold,看它是left fold还是right fold我们可以根据参数包…所在的位置来判断,当参数包…在操作符右边的时候就是right fold,在左边的时候就是left fold。示例如下:
template<typename... Args>
auto add_val(Args&&... args) {
return (args + ...);
}
auto t = add_val(1,2,3,4); //10
(2)一元左折叠
对于+这种满足交换律的操作符来说,left fold和right fold是一样的,比如上面的例子你也可以写成left fold。
template<typename... Args>
auto add_val(Args&&... args)
{
return (... + args);
}
auto t = add_val(1,2,3,4); //10
对于不满足交换律的操作符来说就要注意了,比如减法,下面的right fold和left fold的结果就不一样。
template<typename... Args>
auto sub_val_right(Args&&... args) {
return (args - ...);
}
template<typename... Args>
auto sub_val_left(Args&&... args) {
return (... - args);
}
auto t = sub_val_right(2,3,4); //(2-(3-4)) = 3
auto t1 = sub_val_left(2,3,4); //((2-3)-4) = -5
(3)二元右折叠
二元fold的语义和一元fold的语义是相同的,参数包…在左即二元左折叠,参数包…在右即右折叠。下面看一个二元右折叠的例子。
template<typename... Args>
auto sub_one_left(Args&&... args) {
return (1 - ... - args);
}
auto t = sub_one_right(2,3,4);//(2-(3-(4-1))) = 2
(4)二元左折叠
template<typename... Args>
auto sub_one_right(Args&&... args) {
return (args - ... - 1);
}
auto t = sub_one_left(2,3,4);// (((1-2)-3)-4) = -8
(5)comma fold
在C++17之前,我们经常使用逗号表达式结合列表初始化的方式对参数包进行展开,比如像下面这个例子:
template<typename T>
void print_arg(T t) {
std::cout << t << std::endl;
}
template<typename... Args>
void print2(Args... args) {
int a[] = { (print_arg(args), 0)... };
//或者
//std::initializer_list<int>{(print_arg(args), 0)...};
}
这种写法比较繁琐,用fold expression就会变得很简单了。
//unary right fold
template<typename... Args>
void print3(Args... args) {
(print_arg(args), ...);
}
//unary left fold
template<typename... Args>
void print3(Args... args) {
(..., print_arg(args));
}
unary right fold 和 unary left fold,对于 comma 来说两种写法是一样的,参数都是从左至右传入 print_arg 函数。当然,我们也可以通过 binary fold 实现:
template<typename ...Args>
void printer(Args&&... args) {
(std::cout << ... << args) << '\n';
}
注意,下面的写法是不合法的,根据binary fold的语法,参数包…必须在操作符中间。
template<typename ...Args>
void printer(Args&&... args) {
(std::cout << args << ...) << '\n';
}