可变参数模板
可变参数模板允许你定义一个可以接受任意数量、任意类型参数的模板函数或模板类。在C++11之前,如果你想写一个能接受任意数量参数的函数,你只能使用C风格的可变参数(如printf类型不安全:编译器无法检查传入参数的类型是否与格式字符串匹配。无法处理非POD(Plain Old Data)类型:如等。需要复杂的运行时解析逻辑:使用va_listva_startva_argva_end。可变参数模板完美
可变参数模板
C++11中的可变参数模板(Variadic Templates),它极大地增强了C++模板的灵活性和表现力,是实现诸如std::tuple, std::function, std::make_shared等现代库组件的基石。
我们将从基础概念开始,逐步深入到高级用法和实现技巧。
1. 什么是可变参数模板?
可变参数模板允许你定义一个可以接受任意数量、任意类型参数的模板函数或模板类。
在C++11之前,如果你想写一个能接受任意数量参数的函数,你只能使用C风格的可变参数(如printf),但这有诸多缺点:
- 类型不安全:编译器无法检查传入参数的类型是否与格式字符串匹配。
- 无法处理非POD(Plain Old Data)类型:如
std::string等。 - 需要复杂的运行时解析逻辑:使用
va_list,va_start,va_arg,va_end。
可变参数模板完美地解决了所有这些问题,它在编译期进行类型推导和展开,是类型安全且高效的。
2. 基本语法
a. 模板参数包 (Template Parameter Pack)
使用省略号...在模板参数列表中来声明一个模板参数包。
// Args 是一个模板参数包,代表0个或多个模板类型参数
template <typename... Args>
void myFunction(Args... args) { // args 是一个函数参数包
// ...
}
typename... Args:Args是一个模板参数包,它可以绑定到零个、一个或多个模板类型参数。Args... args:args是一个函数参数包,它的类型是Args,它的值是传递给函数的所有参数。
示例:
// 可以这样调用
myFunction(); // Args 和 args 为空包
myFunction(1); // Args 是 <int>, args 是 (1)
myFunction(1, 2.2, "hello", std::string("world"));
// Args 是 <int, double, const char*, std::string>
// args 是 (1, 2.2, "hello", std::string("world"))
b. sizeof... 运算符
用于在编译时获取参数包中包含的参数个数。
template <typename... Args>
void countArgs(Args... args) {
std::cout << "Number of types in template pack: " << sizeof...(Args) << std::endl;
std::cout << "Number of arguments in function pack: " << sizeof...(args) << std::endl;
}
int main() {
countArgs(1, 2.0, 'a'); // 输出:3 和 3
return 0;
}
3. 参数包展开 (Pack Expansion)
参数包本身不能直接使用,必须通过展开(Expansion)来使用。展开的方式是在模式(Pattern)后面跟上省略号...,编译器会将这个模式应用于参数包中的每一个元素。
基本形式:模式...
a. 递归展开 (Recursive Expansion)
这是最经典和常用的方法。你需要一个终止函数(边界条件) 和一个递归函数。
示例:递归打印所有参数
#include <iostream>
// 1. 终止函数:当参数包为空时调用此函数
void print() {
std::cout << "End\\n";
}
// 2. 递归函数模板:处理至少一个参数的通用情况
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " "; // 处理第一个参数
print(rest...); // 递归调用,展开参数包 rest
}
int main() {
print(1, 2.2, "Hello", 'c'); // 输出: 1 2.2 Hello c End
return 0;
}
过程解析:
print(1, 2.2, "Hello", 'c'):T是int,first=1,rest是[2.2, "Hello", 'c']。- 打印
1,然后调用print(2.2, "Hello", 'c')。
- 打印
print(2.2, "Hello", 'c'):T是double,first=2.2,rest是["Hello", 'c']。- 打印
2.2,然后调用print("Hello", 'c')。
- 打印
print("Hello", 'c'):T是const char*,first="Hello",rest是['c']。- 打印
"Hello",然后调用print('c')。
- 打印
print('c'):T是char,first='c',rest是空包[]。- 打印
'c',然后调用print()。
- 打印
print():调用终止函数,打印"End"。
b. 折叠表达式 (C++17, Fold Expressions)
C++17引入了折叠表达式,它提供了一种更简洁、更高效(通常编译结果更好)的方式来展开参数包。虽然这是C++17的特性,但因为它与可变参数模板紧密相关且极其重要,必须在这里介绍。
它有四种形式:
( pack op ... ):一元右折叠( ... op pack ):一元左折叠( init op ... op pack ):二元右折叠( pack op ... op init ):二元左折叠
op可以是很多运算符,如+, -, *, /, %, <<, >>, ,, &&, ||, &, |等。
示例:
#include <iostream>
// 使用折叠表达式实现打印 (C++17)
template <typename... Args>
void print(Args... args) {
// 一元右折叠,展开形式为:(std::cout << args) << ...)
// 相当于:((std::cout << arg1) << arg2) << ... << argN)
(std::cout << ... << args) << std::endl;
// 你也可以用逗号运算符和lambda来模拟更复杂的操作
// 下面这行代码会按顺序执行所有lambda,但只输出最后一个lambda的结果(42)
// ((std::cout << args << " "), ...);
// std::cout << std::endl;
}
// 计算所有参数的和
template <typename... Args>
auto sum(Args... args) {
// 二元左折叠,展开形式为:(((init + arg1) + arg2) + ... + argN)
return (0 + ... + args);
}
// 检查所有参数是否都为true (逻辑与)
template <typename... Args>
bool allTrue(Args... args) {
// 一元左折叠,展开形式为:(... && args)
// 相当于:((arg1 && arg2) && ... && argN)
return (... && args);
}
int main() {
print(1, 2.2, "Hello"); // 输出: 12.2Hello
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // 输出: 15
std::cout << std::boolalpha << allTrue(true, true, false) << std::endl; // 输出: false
return 0;
}
4. 高级用法与技巧
a. 完美转发参数包 (Perfect Forwarding)
这是实现像std::make_shared这样的工厂函数的关键。我们使用std::forward来保持参数的左值/右值属性。
#include <utility> // for std::forward
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) { // 注意万能引用 &&
// 使用 std::forward<Args> 来完美转发每个参数
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
class MyClass {
public:
MyClass(int a, const std::string& b) {
std::cout << "Constructed with " << a << " and " << b << std::endl;
}
};
int main() {
// 完美转发 int(42) 和 std::string("Test")
auto obj = make_unique<MyClass>(42, "Test");
return 0;
}
std::forward<Args>(args)...:这个展开非常强大。它会根据Args中每个参数的实际类型(是左值引用还是右值引用),对args包中的每个参数分别应用std::forward。- 如果传入的是右值,则转发为右值(允许移动)。
- 如果传入的是左值,则转发为左值(保持拷贝)。
b. 模板特化与参数包
可变参数模板也可以被特化。
示例:实现一个判断类型是否在列表中的模板
#include <iostream>
// 基础模板:默认不在列表中
template <typename T, typename... List>
struct is_one_of : std::false_type {};
// 特化1:当列表的第一个类型就匹配时
template <typename T, typename... Rest>
struct is_one_of<T, T, Rest...> : std::true_type {};
// 特化2:当第一个类型不匹配,递归检查剩余列表
template <typename T, typename First, typename... Rest>
struct is_one_of<T, First, Rest...> : is_one_of<T, Rest...> {};
// 辅助变量模板 (C++17)
template <typename T, typename... List>
inline constexpr bool is_one_of_v = is_one_of<T, List...>::value;
int main() {
std::cout << std::boolalpha;
std::cout << is_one_of_v<int, float, double, char> << std::endl; // false
std::cout << is_one_of_v<int, float, double, int, char> << std::endl; // true
return 0;
}
c. 参数包与constexpr if (C++17)
C++17的constexpr if可以极大地简化递归终止条件的编写,无需单独写一个终止函数。
#include <iostream>
template <typename T>
void print_single(const T& value) {
std::cout << value << " ";
}
template <typename... Args>
void print(Args... args) {
// 使用初始化列表和逗号运算符来展开包
// 展开为:{ (print_single(args), 0), ... }
// 这保证了执行的顺序性
((print_single(args)), ...);
std::cout << std::endl;
}
// 使用 constexpr if 的递归版本 (更清晰)
template <typename T, typename... Args>
void print_recursive(const T& first, const Args&... rest) {
std::cout << first << " ";
if constexpr (sizeof...(rest) > 0) { // 编译期if,如果还有参数...
print_recursive(rest...); // ...才进行递归调用
} else {
std::cout << std::endl;
}
}
int main() {
print(1, 2.2, "Hello");
print_recursive(1, 2.2, "Hello");
return 0;
}
if constexpr在编译期判断条件,如果参数包rest为空,print_recursive(rest...)这行代码甚至不会被实例化,从而避免了需要终止函数的问题。
5. 可变参数模板类
可变参数模板不仅用于函数,也用于类,例如std::tuple。
示例:实现一个简单的Tuple
#include <iostream>
// 前向声明
template <typename... Types>
class Tuple;
// 基本模板:空元组
template <>
class Tuple<> {};
// 递归模板定义:一个元素 + 剩余元素的元组
template <typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
public:
Tuple(const Head& h, const Tail&... t) : Tuple<Tail...>(t...), head_(h) {}
// 获取头元素 (第0个元素)
Head& getHead() { return head_; }
const Head& getHead() const { return head_; }
// 获取尾元组 (包含从1开始的所有元素)
Tuple<Tail...>& getTail() { return *this; }
const Tuple<Tail...>& getTail() const { return *this; }
private:
Head head_;
};
// 辅助函数:获取第N个元素
template <unsigned N, typename... Types>
auto& get(Tuple<Types...>& t) {
if constexpr (N == 0) {
return t.getHead();
} else {
return get<N - 1>(t.getTail());
}
}
int main() {
Tuple<int, double, std::string> myTuple(42, 3.14, "Metaprogramming");
std::cout << get<0>(myTuple) << std::endl; // 42
std::cout << get<1>(myTuple) << std::endl; // 3.14
std::cout << get<2>(myTuple) << std::endl; // "Metaprogramming"
return 0;
}
这个简单的Tuple通过递归继承实现。Tuple<int, double, std::string>的继承 hierarchy 是:Tuple<int, double, std::string> : Tuple<double, std::string> : Tuple<std::string> : Tuple<>。
每个层级存储自己对应的元素(head_),并通过getTail()返回基类(即存储剩余元素的子元组)。get<N>函数通过递归下降来访问正确的层级。
总结
| 特性/技巧 | 描述 | 关键点 |
|---|---|---|
| 基本语法 | template <typename... Args> |
声明模板和函数参数包 |
sizeof... |
获取参数包大小 | 编译期常量 |
| 递归展开 | 使用终止函数+递归函数模板 | 经典方法,通用性强 |
| 折叠表达式 (C++17) | (op ... pack) 等 |
简洁高效,替代很多递归场景 |
| 完美转发 | std::forward<Args>(args)... |
实现工厂函数的关键,保持值类别 |
| 模板特化 | 对参数包进行模式匹配 | 用于类型计算、编译期判断 |
constexpr if (C++17) |
编译期条件判断 | 简化递归终止,避免多余函数重载 |
| 可变参数类模板 | 如std::tuple, std::variant |
通过递归继承或递归复合实现 |
更多推荐

所有评论(0)