可变参数模板与折叠表达式
00:00
C++可变参数模板与折叠表达式详解。
概述
可变参数模板(Variadic Templates)和折叠表达式(Fold Expressions)是 C++ 模板元编程的核心工具。可变参数模板允许模板接受任意数量和类型的参数,而 C++17 引入的折叠表达式则极大地简化了参数包的操作方式。两者结合使得编写类型安全、零开销的泛型代码变得直观而高效。
从 C++11 的递归展开到 C++17 的折叠表达式,再到 C++20 的 Lambda 捕获包展开,参数包的处理方式越来越简洁。理解这些机制是掌握现代 C++ 模板编程的必经之路。
基础概念
参数包的类型
- 模板参数包:
typename... Types,匹配零个或多个类型 - 非类型参数包:
int... Values,匹配零个或多个值 - 模板模板参数包:
template<typename> class... Tmpls,匹配零个或多个模板
参数包展开的位置
参数包可以在以下位置展开:
- 函数参数列表:
func(args...) - 模板参数列表:
Class<Types...> - 初始化列表:
{args...} - 基类列表:
class Derived : public Bases... - 成员初始化列表:
Bases()...
快速上手
基本的可变参数模板
#include <iostream>
// 使用折叠表达式打印所有参数
template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...); // C++17 折叠表达式
std::cout << '\n';
}
// 使用
print(1, "hello", 3.14); // 输出: 1 hello 3.14
print("仅一个参数"); // 输出: 仅一个参数
print(); // 输出: (空行)
折叠表达式的四种形式
// 1. 一元右折叠: (pack op ...)
// 展开为: e1 op (e2 op (... op en))
template<typename... Args>
auto sumRight(Args... args) {
return (args + ...); // 右折叠求和
}
// 2. 一元左折叠: (... op pack)
// 展开为: ((e1 op e2) op e3) op ...
template<typename... Args>
auto sumLeft(Args... args) {
return (... + args); // 左折叠求和
}
// 3. 二元右折叠: (pack op ... op init)
// 空包时返回 init
template<typename... Args>
auto sumSafe(Args... args) {
return (args + ... + 0); // 带初始值,空包返回 0
}
// 4. 二元左折叠: (init op ... op pack)
// 空包时返回 init
template<typename... Args>
bool allTrue(Args... args) {
return (true && ... && args); // 空包返回 true
}
// 验证
static_assert(sumRight(1, 2, 3) == 6);
static_assert(sumLeft(1, 2, 3) == 6);
static_assert(sumSafe() == 0); // 空包安全
static_assert(allTrue(true, true, false) == false);
详细用法
递归式参数包处理(C++11 兼容)
#include <iostream>
// 递归终止条件
void print() { std::cout << std::endl; }
// 递归展开:每次处理第一个参数,剩余参数递归
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print(rest...);
}
// 使用
print(1, 2.5, "hello", 'c'); // 输出: 1, 2.5, hello, c
完美转发参数包
#include <memory>
#include <utility>
// 完美转发所有参数到构造函数
template<typename T, typename... Args>
std::unique_ptr<T> makeUnique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// 多参数构造
class Config {
std::string host_;
int port_;
bool verbose_;
public:
Config(std::string host, int port, bool verbose)
: host_(std::move(host)), port_(port), verbose_(verbose) {}
};
auto cfg = makeUnique<Config>("localhost", 8080, true);
变参模板与继承
// 混入模式:组合多个功能类
template<typename... Mixins>
class Composite : public Mixins... {
public:
Composite(const Mixins&... mixins) : Mixins(mixins)... {}
// 继承所有 Mixin 的方法
using Mixins::method...; // C++17: 继承所有基类的 method
};
// 定义功能类
struct Loggable { void log(const std::string& msg) { std::cout << msg << std::endl; } };
struct Serializable { std::string serialize() { return "{}"; } };
struct Validatable { bool validate() { return true; } };
// 组合使用
using Service = Composite<Loggable, Serializable, Validatable>;
Service svc;
svc.log("启动服务");
auto json = svc.serialize();
bool ok = svc.validate();
折叠表达式的多种运算符
#include <vector>
// 逗号运算符:对每个参数执行操作
template<typename... Args>
void forEach(Args... args) {
((std::cout << args << "\n"), ...); // 每个参数输出一行
}
// 逻辑运算:检查所有/任一条件
template<typename... Predicates>
bool allOf(Predicates... preds) {
return (... && preds()); // 所有谓词都为 true
}
template<typename... Predicates>
bool anyOf(Predicates... preds) {
return (... || preds()); // 任一谓词为 true
}
// 位运算:合并标志
template<typename... Flags>
uint32_t combineFlags(Flags... flags) {
return (flags | ...); // 按位或合并
}
常见场景
通用访问者(Visitor)
#include <variant>
#include <string>
// 使用变参模板和折叠表达式实现多态访问者
template<typename... Visitors>
struct Overloaded : Visitors... {
using Visitors::operator()...; // C++17: 继承所有 operator()
};
// 推导指引
template<typename... Visitors>
Overloaded(Visitors...) -> Overloaded<Visitors...>;
// 使用
using Value = std::variant<int, double, std::string>;
Value v = 42;
std::visit(Overloaded{
[](int i) { std::cout << "整数: " << i << std::endl; },
[](double d) { std::cout << "浮点: " << d << std::endl; },
[](const std::string& s) { std::cout << "字符串: " << s << std::endl; }
}, v);
编译期类型检查
#include <type_traits>
// 检查所有类型是否满足条件
template<typename Condition, typename... Types>
constexpr bool allSatisfy = (Condition<Types>::value && ...);
// 检查任一类型是否满足条件
template<typename Condition, typename... Types>
constexpr bool anySatisfy = (Condition<Types>::value || ...);
// 使用
static_assert(allSatisfy<std::is_integral, int, long, short>);
static_assert(!allSatisfy<std::is_integral, int, double>);
static_assert(anySatisfy<std::is_integral, int, double>);
注意事项
- 空参数包的一元折叠行为:
&&返回 true,||返回 false,逗号运算符返回 void,其他运算符的空包是编译错误 - 二元折叠通过提供初始值解决了空包问题,如
(args + ... + 0)对空包返回 0 - 折叠表达式只能使用特定的运算符,不能使用自定义运算符
- 参数包展开时每个元素必须是相同的”模式”,如
std::forward<Args>(args)... - 递归式参数包处理会增加编译时间和实例化深度,应优先使用折叠表达式
进阶用法
C++20 Lambda 捕获包展开
// C++20: 在 Lambda 初始化捕获中展开参数包
template<typename... Args>
auto makeCallback(Args... args) {
// 将所有参数移动捕获到 Lambda 中
return [... captures = std::move(args)]() {
return (captures + ... + 0); // 在 Lambda 内使用折叠表达式
};
}
auto cb = makeCallback(1, 2, 3);
std::cout << cb() << std::endl; // 6
编译期字符串哈希
// 使用变参模板实现编译期字符串哈希
template<char... Chars>
constexpr uint32_t operator""_hash() {
uint32_t h = 0;
((h = h * 31 + static_cast<uint32_t>(Chars)), ...); // 折叠表达式
return h;
}
// 使用字面量运算符
constexpr auto cmd_hash = "start"_hash;