C++11 提供了可变模板参数包, 使函数可以接受任意数量的参数. 但在 C++11
中展开参数包稍显麻烦, 而 C++17 的折叠表达式使得展开参数包变得容易,
其基本语法是使用 (...)
的语法形式进行展开.
支持的操作符
折叠表达式支持 32 个操作符: +
, -
, *
, /
, %
, ^
, &
, |
, =
, <
,
>
, <<
, >>
, +=
, -=
, *=
, /=
, %=
, ^=
, &=
, |=
, <<=
,
>>=
,==
, !=
, <=
, >=
, &&
, ||
, ,
, .*
, ->*
.
折叠分类
C++17 的折叠表达式根据参数包的位置分为左折叠和右折叠,
根据操作的对象数量分为一元折叠和二元折叠.
例 1: 左折叠
//
template <typename ... Ts>
auto sum(Ts ... ts)
{
return (... + ts);
}
//
int the_sum {sum(1, 2, 3, 4, 5)}; // the_sum 的值为 15
//
std::string a {"Hello "};
std::string b {"World"};
std::string the_string {sum(a, b)}; // the_string 的值为 "Hello World"
例 2: 右折叠
//
template <typename ... Ts>
auto sum(Ts ... ts)
{
return (ts + ...);
}
例 1 中, 参数包 ...
位于操作符的左侧, 故尔称为左折叠. 如果 ...
位于操作符右侧, 则称为右折叠, 如例 2 所示. 就例 1 与例 2 而言,
左折叠与右折叠的效果是相同的. 对于 int the_sum {sum(1, 2, 3, 4, 5)};
左折叠的展开方式为 1 + (2 + (3 + (4 + 5)))
, 右折叠的展开方式为
(((1 + 2) + 3) + 4) + 5
.
在例 1 与 例 2 中, 如果参数包包含的参数数量为 0, 即为空包, 会产生编译错误, 如
int the_sum {sum()};
, 大致的错误输出如下
In instantiation of 'auto sum(Ts ...) [with Ts = {}]':
error: fold of empty expansion over operator+
return (... + ts);
若要解决空参数包的编译错误, 针对例 1, 可以加上一个数值 0,
可以解决编译错误又可以使得语义不变, 这个 0 相当于缺省值. 通过加上一个数值,
折叠就变成了二元折叠, 如例 3 所示.
例 3: 二元折叠
template <typename ... Ts>
auto sum(Ts ... ts)
{
// 二元右折叠
return (ts + ... + 0);
// 二元左折叠
// return (0 + ... + ts);
}
此时对于 int the_sum {sum(1, 2, 3, 4, 5)};
,
折叠的展开方式为 1 + (2 + (3 + (4 + (5 + 0))))
.
空参数包
空参数包就是参数包中不含任何参数. 对于大多数操作符, 空参数包将会引发编译错误.
对于 &&
或 ||
, 空参数包是合法的, 其中 &&
的展开结果为 true
, ||
的展开结果为 false
. 在逗号 ,
操作符中, 空参数包也合法, 展开为 void()
.
其它例子
例 4: 计算指定区间内包含指定数值的个数
template <typename R, typename ... Ts>
auto count(const R& range, Ts ... ts)
{
return (std::count(std::begin(range), std::end(range), ts) + ...);
}
...
std::vector<int> v {1, 2, 3, 4, 5};
count(v, 2, 5); // returns 2
count(v, 100, 200); // returns 0
count("abcdefg", 'x', 'y', 'z'); // returns 0
count("abcdefg", 'a', 'd', 'f'); // returns 3
例 5: 检查插入多个元素是否成功
template <typename T, typename ... Ts>
bool insert_all(T &set, Ts ... ts)
{
return (set.insert(ts).second && ...);
}
...
std::set<int> my_set {1, 2, 3};
insert_all(my_set, 4, 5, 6); // Returns true, my_set 值为 {1, 2, 3, 4, 5, 6}
insert_all(my_set, 7, 2, 8); // Returns false, my_set 值为 {1, 2, 3, 4, 5, 6, 7}
// 插入 2 时出错, 8 不会被插入
例 6: 检查给的数值是否全部在指定范围内
最后
- 对于一元右折叠
(E op …)
具体展开为E1 op (… op (EN-1 op EN))
. - 对于一元左折叠
(… op E)
具体展开为(( E1 op E2) op …) op En
. - 对于二元右折叠
(E op … op I)
具体展开为E1 op (… op (EN-1 op (EN op I)))
. - 对于二元左折叠
(I op … op E)
具体展开为(((I op E1) op E2) op …) op E2
.
左折叠与右折叠的语义并非总是相同的. 比如对于加法和乘法, 左折叠与右折叠的语义是相同的, 但是对于减法与除法, 其语义是不同的.
例 7: 左右折叠不同语义
template<typename... Args>
auto left_sub(Args&&... args) {
return (... - args);
}
template<typename... Args>
auto right_sub(Args&&... args) {
return (args - ...);
}
...
auto a = left_sub(2, 3, 4); // ((2 - ) -3 ) - 4) = -5
auto b = right_sub(2, 3, 4); // (2 - (3 - 4)) = 3