跳到主要内容

C++ 17折叠表达式

介绍

C++17引入了一个强大的新特性——折叠表达式(fold expressions),它极大地简化了对参数包(parameter packs)的操作。在C++17之前,处理可变参数模板需要编写递归模板函数或使用复杂的技巧。而有了折叠表达式,我们可以用简洁优雅的语法对参数包中的所有元素应用同一个二元运算符。

基本概念

折叠表达式本质上是将二元运算符"折叠"应用到参数包的所有元素上。就像把一系列的值通过指定的运算符连接起来一样。

C++17提供了四种形式的折叠表达式:

  1. 一元右折叠(pack op ...)
    展开为:(E1 op (... op (EN-1 op EN)))

  2. 一元左折叠(... op pack)
    展开为:(((E1 op E2) op ...) op EN)

  3. 二元右折叠(pack op ... op init)
    展开为:(E1 op (... op (EN op init)))

  4. 二元左折叠(init op ... op pack)
    展开为:(((init op E1) op ...) op EN)

其中,op是一个二元运算符,pack是一个参数包,init是一个初始值。

支持的运算符

折叠表达式支持以下C++二元运算符:

+  -  *  /  %  ^  &  |  ~  =  <  >  +=  -=  *=  /=
%= ^= &= |= << >> <<= >>= == != <= >=
&& || , .* ->*
备注

对于某些运算符(如+, *, &, |等),当参数包为空时,会使用特定的值作为结果。例如,空参数包上的+折叠表达式结果为0*的结果为1&&的结果为true

基本示例

让我们看一些简单的例子来理解折叠表达式的工作方式:

示例1:求和

cpp
#include <iostream>

template<typename... Args>
auto sum(Args... args) {
return (... + args); // 一元左折叠
}

int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // 输出: 15
return 0;
}

这个例子中,(... + args) 被展开为 (((1 + 2) + 3) + 4) + 5,结果为15。

示例2:打印所有参数

cpp
#include <iostream>

template<typename... Args>
void print(Args... args) {
// 使用逗号运算符和流插入运算符
(std::cout << ... << args) << std::endl;
}

int main() {
print("Hello", ' ', "World", '!'); // 输出: Hello World!
return 0;
}

这里,(std::cout << ... << args) 被展开为 (((std::cout << "Hello") << ' ') << "World") << '!'

实际应用场景

1. 参数有效性检查

cpp
#include <iostream>
#include <stdexcept>

template<typename... Args>
void validateAll(Args... args) {
if (!(... && args)) { // 检查所有参数是否都为true
throw std::invalid_argument("Validation failed!");
}
std::cout << "All validations passed!" << std::endl;
}

bool isPositive(int n) {
return n > 0;
}

bool isEven(int n) {
return n % 2 == 0;
}

int main() {
try {
validateAll(isPositive(5), isEven(6), isPositive(7));
// 输出: All validations passed!

validateAll(isPositive(5), isEven(7), isPositive(7));
// 抛出异常,因为7不是偶数
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
return 0;
}

2. 实现调用多个函数

cpp
#include <iostream>

// 定义一些函数
void func1() { std::cout << "Function 1 called" << std::endl; }
void func2() { std::cout << "Function 2 called" << std::endl; }
void func3() { std::cout << "Function 3 called" << std::endl; }

// 调用多个函数的模板
template<typename... Functions>
void callAll(Functions... funcs) {
// 使用逗号运算符依次调用所有函数
(funcs(), ...);
}

int main() {
callAll(func1, func2, func3);
/*
输出:
Function 1 called
Function 2 called
Function 3 called
*/
return 0;
}

3. 构建复杂的字符串

cpp
#include <iostream>
#include <string>
#include <sstream>

template<typename... Args>
std::string buildString(Args... args) {
std::ostringstream oss;
// 将所有参数添加到字符串流中
(oss << ... << args);
return oss.str();
}

int main() {
std::string result = buildString("The answer is ", 42, "!\n", "Isn't that ", true, "?");
std::cout << result;
/*
输出:
The answer is 42!
Isn't that 1?
*/
return 0;
}

4. 递归数据结构遍历

cpp
#include <iostream>
#include <vector>
#include <tuple>

// 打印tuple中的所有元素
template<typename Tuple, std::size_t... Is>
void printTupleImpl(const Tuple& t, std::index_sequence<Is...>) {
// 使用折叠表达式打印每个元素,用逗号运算符分隔
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
std::cout << std::endl;
}

template<typename... Args>
void printTuple(const std::tuple<Args...>& t) {
std::cout << "(";
printTupleImpl(t, std::index_sequence_for<Args...>{});
std::cout << ")";
}

int main() {
auto t = std::make_tuple(1, "Hello", 3.14, 'A');
printTuple(t); // 输出: (1, Hello, 3.14, A)
return 0;
}

进阶技巧

使用二元折叠表达式

二元折叠表达式允许我们提供一个初始值:

cpp
#include <iostream>
#include <string>

template<typename... Args>
auto multiply(Args... args) {
return (args * ... * 1); // 二元右折叠,提供初始值1
}

int main() {
std::cout << multiply(2, 3, 4) << std::endl; // 输出: 24
std::cout << multiply() << std::endl; // 输出: 1 (空参数包)
return 0;
}

组合多个折叠表达式

cpp
#include <iostream>
#include <vector>

template<typename... Args>
void pushBackAll(std::vector<int>& vec, Args... args) {
(vec.push_back(args), ...); // 一元右折叠
}

template<typename... Args>
bool allPositive(Args... args) {
return ((args > 0) && ...); // 一元右折叠
}

int main() {
std::vector<int> numbers;
pushBackAll(numbers, 1, 2, 3, 4, 5);

for(int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl;
// 输出: 1 2 3 4 5

std::cout << "All positive: " << std::boolalpha
<< allPositive(1, 2, 3, 4, 5) << std::endl;
// 输出: All positive: true

std::cout << "All positive: " << std::boolalpha
<< allPositive(1, -2, 3) << std::endl;
// 输出: All positive: false

return 0;
}

注意事项与限制

  1. 空参数包:某些运算符在用于空参数包时有特殊规则:

    • && 折叠在空参数包上的值为 true
    • || 折叠在空参数包上的值为 false
    • , 折叠在空参数包上的值为 void()
    • 对于其他大多数运算符,折叠空参数包是非法的
  2. 运算优先级:折叠表达式中的运算顺序是明确定义的,但要注意运算符的优先级。

  3. 类型兼容性:参数包中的所有元素必须能够与所选的运算符一起使用。

总结

C++17的折叠表达式提供了一种简洁优雅的方式来处理参数包,极大地简化了可变参数模板的使用。它们让我们能够:

  • 对参数包中的所有元素应用同一个二元运算
  • 减少递归模板的使用
  • 编写更简洁清晰的代码

折叠表达式特别适合需要将操作应用于多个参数的情况,如求和、连接、验证等。通过掌握这一强大的特性,你可以编写更加简洁和可维护的C++代码。

练习

  1. 实现一个函数,接受任意数量的字符串,并用指定的分隔符连接它们。
  2. 创建一个函数,检查所有传入的数字是否都满足特定条件(例如,是否都是偶数)。
  3. 编写一个函数,将任意数量的参数添加到一个容器(如std::vector)中。
  4. 实现一个函数,计算任意数量参数的平均值。
  5. 创建一个函数,找出传入的所有参数中的最大值。

补充资源

  • C++ Reference: 折叠表达式
  • C++17标准文档 (ISO/IEC 14882:2017)
  • 《Effective Modern C++》by Scott Meyers
  • 《C++17 in Detail》by Bartlomiej Filipek

通过理解并应用C++17的折叠表达式,你能够编写出更加简洁、表达力更强的代码,尤其是在处理可变参数模板时。随着练习的深入,你会发现这一特性在许多场景中都非常有用。