C++ Template: 可变参数模板 Variadic Templates
整理可变参数模板、参数包展开、折叠表达式和变长基类的常见写法。
可变参数模板 Variadic Templates 是 C++11 引入的一种特性, 它允许模板接收可变数量的模板参数或函数参数, 这使得编写灵活、通用的代码变得更加容易
可变参数模板使用 ... (省略号) 表示可以接受多个参数, 这些参数可以是 模板参数 或 函数参数
主要有两种形式:
-
模板参数包: 定义在模板参数列表中, 例如
typename... Args -
函数参数包: 定义在函数参数列表中, 例如
Args... args
函数模板中的可变参数
用可变参数模板编写接受多个参数的函数
#include <iostream>
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n'; // 使用折叠表达式
}
int main() {
print(1, 2, 3, "Hello", 4.5); // 输出:123Hello4.5
return 0;
}这里, Args... args 是 函数参数包, 它包含了传入的所有参数
类模板中的可变参数
可变参数也可以应用于类模板
template<typename... Args>
class Tuple {
// 可变参数包存储为类型
};
Tuple<int, double, std::string> t; // 包含多个模板参数递归展开可变参数包
在不使用折叠表达式的情况下, 可以递归处理参数包
#include <iostream>
// 递归函数模板
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first << " ";
print(rest...); // 递归调用,展开剩余参数
}
// 基本情况:参数包为空时的函数
void print() {
std::cout << '\n';
}
int main() {
print(1, 2, 3, "Hello", 4.5); // 输出:1 2 3 Hello 4.5
return 0;
}折叠表达式 Fold Expressions
折叠表达式 Fold Expressions 是C++17引入的一个新特性, 用于简化对可变参数模板的操作, 它允许我们通过二元操作符对参数包中的所有参数进行操作, 并可以选择一个初始值
无初始值的折叠
-
左折叠
( ... op pack )((pack1 op pack2) op pack3) op ... op packNtemplate<typename... T> auto leftFold(T... args) { return (... + args); // 左折叠 } leftFold(1, 2, 3, 4); // 等价于 ((1 + 2) + 3) + 4 -
右折叠
( pack op ... )pack1 op (pack2 op (pack3 op ... op packN))template<typename... T> auto rightFold(T... args) { return (args + ...); // 右折叠 } rightFold(1, 2, 3, 4); // 等价于 1 + (2 + (3 + 4))
带初始值的折叠
-
左折叠
( init op ... op pack )(((init op pack1) op pack2) op pack3) op ... op packNtemplate<typename... T> auto leftFoldWithInit(T... args) { return (0 + ... + args); // 左折叠,初始值为 0 } leftFoldWithInit(1, 2, 3, 4); // 等价于 (((0 + 1) + 2) + 3) + 4 -
右折叠
( pack op ... op init )pack1 op (pack2 op (pack3 op ... op (packN op init)))template<typename... T> auto rightFoldWithInit(T... args) { return (args + ... + 0); // 右折叠,初始值为 0 } rightFoldWithInit(1, 2, 3, 4); // 等价于 1 + (2 + (3 + (4 + 0)))
实例
简单的加法折叠
template<typename... T>
auto foldSum(T... s) {
return (... + s); // 左折叠
}调用
int sum = foldSum(1, 2, 3, 4); // 结果为 10二叉树路径遍历
可以使用折叠表达式来遍历二叉树的路径
struct Node {
int value;
Node* left;
Node* right;
Node(int i=0) : value(i), left(nullptr), right(nullptr) {}
};
auto left = &Node::left;
auto right = &Node::right;
template<typename T, typename... TP>
Node* traverse(T np, TP... paths) {
return (np ->* ... ->* paths); // 使用折叠表达式遍历路径
}
int main() {
Node* root = new Node{0};
root->left = new Node{1};
root->left->right = new Node{2};
Node* node = traverse(root, left, right); // 遍历到 root->left->right
}带空格的打印函数
默认的折叠表达式不会在参数之间添加空格, 为了解决这个问题, 我们可以创建一个辅助类 AddSpace
template<typename T>
class AddSpace {
private:
T const& ref;
public:
AddSpace(T const& r) : ref(r) {}
friend std::ostream& operator<<(std::ostream& os, AddSpace<T> s) {
return os << s.ref << ' ';
}
};
template<typename... Args>
void print(Args... args) {
(std::cout << ... << AddSpace(args)) << '\n';
}
int main() {
print(1, 2, 3, "Hello"); // 输出: "1 2 3 Hello "
}AddSpace 类使用了 类模板参数推导 Class Template Argument Deduction, 以简化类型的推导过程
特殊情况
对于空参数包
&&操作符返回true||操作符返回false,操作符返回void()
模板参数包 和 函数参数包
模板参数包 和 函数参数包 互相关联的, 模板参数包负责推导类型, 而函数参数包负责接收参数值, 可以理解为, 模板参数包定义了类型的可变性, 而函数参数包实现了具体值的操作
使用模板参数包
模板参数包用在类模板或函数模板的定义中
类型计数器
模板参数包用于处理类型信息
#include <iostream>
#include <type_traits>
template<typename... Types>
struct TypeCounter {
static constexpr std::size_t count = sizeof...(Types);
};
int main() {
std::cout << "Number of types: " << TypeCounter<int, double, char>::count << '\n';
return 0;
}输出
Number of types: 3静态断言类型一致性 有时, 我们可能只用模板参数包来处理类型, 而函数参数并不涉及参数包
#include <iostream>
#include <type_traits>
template<typename T, typename... Args>
void checkTypes() {
static_assert((std::is_same_v<T, Args> && ...), "All types must be the same!");
}
int main() {
checkTypes<int, int, int>(); // 正常编译
// checkTypes<int, double, int>(); // 编译错误:类型不一致
return 0;
}同时使用模板参数包和函数参数包
函数参数包在没有模板参数包的基础下使用, 例如函数重载或递归处理
递归打印
#include <iostream>
// 使用函数参数包处理变长参数
void print() {
std::cout << "End of parameters.\n";
}
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first << " ";
print(rest...); // 递归调用
}
int main() {
print(1, 2.5, "hello", 'c'); // 输出:1 2.5 hello c End of parameters.
return 0;
}函数参数包无需模板参数推导
当函数模板有显式的模板参数时, 函数参数包并不依赖模板参数推导
类型已知但参数个数未知
#include <iostream>
template<typename T>
void printArgs(T arg) {
std::cout << arg << '\n';
}
template<typename T>
void printAllArgs(T firstArg, ...) { // 使用变长参数
printArgs(firstArg);
}
int main() {
printAllArgs<int>(42, 3.14, "hello"); // 只打印 42,因为不展开参数
return 0;
}这种用途可能有限, 因为它不会执行参数展开
变长基类 Variadic Base Classes
派生类 不会自动继承基类的 () 运算符 (或其他 非虚成员函数), 除非显式使用 using 声明将基类的 operator() 引入派生类的作用域, 这是 C++ 的一个语言设计特性, 用于避免潜在的命名冲突
可以使用可变参数模板定义 变长基类, 然后通过 using 声明将基类的成员 (如 operator()) 引入派生类, 这种技术非常有用, 尤其在需要组合多个功能时,例如实现策略模式或函数对象的组合
例如有以下几个基础类
Customer 类: 表示客户, 包含一个 name 字段及其访问器
class Customer {
private:
std::string name;
public:
Customer(std::string const& n) : name(n) {}
std::string getName() const { return name; }
};CustomerEq 类: 用于比较两个 Customer 对象是否相等
struct CustomerEq {
bool operator()(Customer const& c1, Customer const& c2) const {
return c1.getName() == c2.getName();
}
};CustomerHash 类: 用于计算 Customer 对象的哈希值
struct CustomerHash {
std::size_t operator()(Customer const& c) const {
return std::hash<std::string>()(c.getName());
}
};变长基类的定义
为了将 CustomerHash 和 CustomerEq 的 operator() 同时引入一个类中, 我们定义了 Overloader 模板类, 它可以继承任意数量的基类, 并将这些基类的 operator() 引入到派生类
template<typename... Bases>
struct Overloader : Bases... {
using Bases::operator()...; // C++17: 将所有基类的 operator() 引入当前类
};- 继承所有基类:
Overloader使用可变参数模板继承了任意数量的基类 - 引入基类的成员:
using Bases::operator()...;将每个基类的operator()声明引入当前作用域
组合 CustomerHash 和 CustomerEq
using CustomerOP = Overloader<CustomerHash, CustomerEq>;CustomerOP 是一个同时从 CustomerHash 和 CustomerEq 派生的类, 并且具有两个 operator()
- 一个用于哈希计算 (来自
CustomerHash) - 一个用于相等性比较 (来自
CustomerEq)
用于 std::unordered_set
std::unordered_set 是 C++ 标准库中的哈希表实现, 模板参数定义如下
template<
typename Key, // 键的类型
typename Hash = std::hash<Key>, // 哈希函数,默认是 std::hash<Key>
typename KeyEqual = std::equal_to<Key>, // 相等比较函数,默认是 std::equal_to<Key>
typename Allocator = std::allocator<Key> // 内存分配器,默认是 std::allocator<Key>
>
class unordered_set;从定义中可以看到:
-
Key: 键的类型, 即集合中存储的元素类型
-
Hash: 用于计算键的哈希值的类型 (必须是一个可调用对象, 通常实现
operator()) -
KeyEqual: 用于判断两个键是否相等的类型 (必须是一个可调用对象, 通常实现
operator()) -
Allocator: 分配器类型 (通常使用默认值即可)
我们可以将 CustomerOP 用作哈希函数和比较函数的类型
std::unordered_set<Customer, CustomerHash, CustomerEq> coll1; // 使用单独的哈希和比较
std::unordered_set<Customer, CustomerOP, CustomerOP> coll2; // 使用组合的功能对象coll1: 需要分别传入CustomerHash(提供operator(), 用于计算Customer对象的哈希值) 和CustomerEq(提供operator(), 用于比较两个Customer是否相等)coll2: 只需传入CustomerOP, 因为它同时具备哈希和比较的能力
完整代码
#include <string>
#include <unordered_set>
#include <iostream>
// 定义 Customer 类
class Customer {
private:
std::string name;
public:
Customer(std::string const& n) : name(n) {}
std::string getName() const { return name; }
};
// 比较器:比较两个 Customer 对象是否相等
struct CustomerEq {
bool operator()(Customer const& c1, Customer const& c2) const {
return c1.getName() == c2.getName();
}
};
// 哈希器:计算 Customer 对象的哈希值
struct CustomerHash {
std::size_t operator()(Customer const& c) const {
return std::hash<std::string>()(c.getName());
}
};
// 定义 Overloader 模板类
template<typename... Bases>
struct Overloader : Bases... {
using Bases::operator()...; // 将所有基类的 operator() 引入作用域
};
int main() {
// 定义组合的功能对象类型
using CustomerOP = Overloader<CustomerHash, CustomerEq>;
// 使用单独的哈希和比较
std::unordered_set<Customer, CustomerHash, CustomerEq> coll1;
// 使用组合后的功能对象
std::unordered_set<Customer, CustomerOP, CustomerOP> coll2;
coll2.emplace("Alice");
coll2.emplace("Bob");
Customer c("Alice");
if (coll2.find(c) != coll2.end()) {
std::cout << c.getName() << " is found!" << std::endl;
}
return 0;
}输出
Alice is found!总结
在 C++ 中, 继承基类的 operator() 并不能自动将其引入派生类的作用域 (尤其是模板基类), 为了能够直接在派生类中使用基类的 operator(), 需要显式声明 using
using Bases::operator()...; // 将所有基类的 operator() 引入当前类通过使用 Overloader, 将多个功能对象合并为一个类, 无需手动实现多个 operator(), 通过可变参数模板, 可以组合任意数量和类型的功能对象
using Bases::operator()...;是 C++17 的特性, 允许使用 折叠表达式 将多个基类的成员引入当前作用域