目录
[参数包(Parameter Pack)](#参数包(Parameter Pack))
[sizeof... 运算符](#sizeof... 运算符)
[四、折叠表达式(C++17 简化)](#四、折叠表达式(C++17 简化))
[六、tuple 的实现原理](#六、tuple 的实现原理)
[简化版 tuple 实现](#简化版 tuple 实现)
[七、完整例子:类型安全的 printf](#七、完整例子:类型安全的 printf)
[1. 忘记递归终止函数](#1. 忘记递归终止函数)
[2. 在运行时使用 sizeof...](#2. 在运行时使用 sizeof...)
[3. 参数包展开顺序错误](#3. 参数包展开顺序错误)
一、为什么需要可变参数模板?
在 C++98/03 中,实现一个任意数量参数的函数很麻烦:
cpp
// 方式1:重载(最多 N 个)
void print() {}
void print(int a) {}
void print(int a, int b) {}
void print(int a, int b, int c) {}
// 永远不够...
// 方式2:使用 va_list(类型不安全)
#include <cstdarg>
void badPrint(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
// 容易出错,没有类型检查
vprintf(fmt, args);
va_end(args);
}
badPrint("%d %s %f", 42, "hello", 3.14); // 格式字符串必须匹配
可变参数模板:类型安全、编译期检查、无限参数。
cpp
template<typename... Args>
void printAll(Args... args) {
// 可以处理任意数量的参数,每个都有自己的类型
}
printAll(1, "hello", 3.14, 'c'); // 编译期展开
二、基本语法
参数包(Parameter Pack)
cpp
// Args 是类型参数包
template<typename... Args>
class Tuple {};
// args 是函数参数包
template<typename... Args>
void func(Args... args) {}
// 使用
Tuple<int, double, string> t; // Args = {int, double, string}
func(1, 2.5, "hello"); // args = {1, 2.5, "hello"}
sizeof... 运算符
获取参数包中的参数个数(编译期常量):
cpp
template<typename... Args>
void countArgs(Args... args) {
cout << "类型数量: " << sizeof...(Args) << endl;
cout << "参数数量: " << sizeof...(args) << endl;
}
countArgs(1, "hello", 3.14); // 类型数量: 3,参数数量: 3
countArgs(); // 类型数量: 0,参数数量: 0
三、参数包展开
参数包不能直接使用,需要通过展开获取每个元素。主要有两种展开方式。
方式1:递归展开(最直观)
cpp
// 终止函数:空参数时调用
void print() {
cout << endl;
}
// 递归函数:处理第一个参数,然后递归处理剩余
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
cout << first << " "; // 处理第一个
print(rest...); // 递归处理剩余
}
int main() {
print(1, 2.5, "hello", 'c'); // 输出: 1 2.5 hello c
}
展开过程:
text
print(1, 2.5, "hello", 'c')
→ cout << 1; print(2.5, "hello", 'c')
→ cout << 2.5; print("hello", 'c')
→ cout << "hello"; print('c')
→ cout << 'c'; print()
→ 换行
方式2:初始化列表展开(C++11,更高效)
利用数组初始化列表的求值顺序保证(从左到右),一次性展开所有参数:
cpp
template<typename... Args>
void printAll(Args... args) {
// 使用 int 数组的初始化列表来触发展开
int dummy[] = { (cout << args << " ", 0)... };
// 或者更优雅的方式:
// (cout << ... << args); // C++17 折叠表达式,后面会讲
}
printAll(1, 2.5, "hello"); // 输出: 1 2.5 hello
解释:
-
(cout << args << " ", 0)是一个逗号表达式:输出args,然后返回0 -
{ (expr)... }将expr对每个args展开,生成{0, 0, 0}数组 -
数组不需要使用,只是为了触发展开
四、折叠表达式(C++17 简化)
C++17 引入了折叠表达式,让参数包展开更加简洁:
cpp
// 一元右折叠
template<typename... Args>
void printAll(Args... args) {
(cout << ... << args) << endl; // ((cout << arg1) << arg2) << ...
}
// 带分隔符
template<typename... Args>
void printWithComma(Args... args) {
((cout << args << (sizeof...(args) == 1 ? "" : ", ")), ...);
cout << endl;
}
// 求和
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 右折叠:arg1 + (arg2 + (arg3 + ...))
}
int main() {
printAll(1, 2.5, "hello"); // 1 2.5 hello
cout << sum(1, 2, 3, 4) << endl; // 10
}
| 折叠类型 | 语法 | 展开结果 |
|---|---|---|
| 一元右折叠 | (args + ...) |
(arg1 + (arg2 + (arg3 + ...))) |
| 一元左折叠 | (... + args) |
(((arg1 + arg2) + arg3) + ...) |
| 二元折叠 | (args + ... + init) |
带初始值 |
五、完美转发与可变参数模板
在库开发中,常常需要将参数包完美转发给另一个函数:
cpp
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
// 完美转发每个参数
return unique_ptr<T>(new T(forward<Args>(args)...));
}
// 使用
auto p = make_unique<string>(10, 'a'); // 等价于 new string(10, 'a')
展开过程 :
forward<Args>(args)... 展开为 forward<Arg1>(arg1), forward<Arg2>(arg2), ...
六、tuple 的实现原理
std::tuple 是可变参数模板的经典应用,可以存储任意多个不同类型的值。
简化版 tuple 实现
cpp
#include <iostream>
#include <typeinfo>
using namespace std;
// 前向声明
template<typename... Types>
class Tuple;
// 空 tuple 的特化(终止条件)
template<>
class Tuple<> {};
// 递归定义:一个头部 + 尾部
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
private:
Head value;
public:
Tuple() : value(), Tuple<Tail...>() {}
Tuple(const Head& h, const Tail&... t)
: value(h), Tuple<Tail...>(t...) {}
// 获取头部
Head& head() { return value; }
const Head& head() const { return value; }
// 获取尾部
Tuple<Tail...>& tail() { return *this; }
const Tuple<Tail...>& tail() const { return *this; }
};
// 获取 tuple 中第 N 个元素(编译期递归)
template<size_t N, typename Tuple>
struct TupleElement;
template<typename Head, typename... Tail>
struct TupleElement<0, Tuple<Head, Tail...>> {
using type = Head;
static Head& get(Tuple<Head, Tail...>& t) {
return t.head();
}
};
template<size_t N, typename Head, typename... Tail>
struct TupleElement<N, Tuple<Head, Tail...>> {
using type = typename TupleElement<N-1, Tuple<Tail...>>::type;
static type& get(Tuple<Head, Tail...>& t) {
return TupleElement<N-1, Tuple<Tail...>>::get(t.tail());
}
};
// get 函数模板
template<size_t N, typename... Types>
auto& get(Tuple<Types...>& t) {
return TupleElement<N, Tuple<Types...>>::get(t);
}
// 打印 tuple(递归展开)
template<size_t N, typename... Types>
void printTuple(const Tuple<Types...>& t) {
if constexpr (N < sizeof...(Types)) {
cout << get<N>(t);
if constexpr (N + 1 < sizeof...(Types)) {
cout << ", ";
}
printTuple<N+1>(t);
}
}
template<typename... Types>
void print(const Tuple<Types...>& t) {
cout << "(";
printTuple<0>(t);
cout << ")" << endl;
}
int main() {
Tuple<int, double, string> t(42, 3.14, "hello");
cout << get<0>(t) << endl; // 42
cout << get<1>(t) << endl; // 3.14
cout << get<2>(t) << endl; // hello
print(t); // (42, 3.14, hello)
return 0;
}
原理:
-
Tuple<A, B, C>继承自Tuple<B, C>,再继承自Tuple<C>,再继承自Tuple<> -
每个层级存储一个元素(
value) -
get<N>通过模板递归找到第 N 层的元素
七、完整例子:类型安全的 printf
cpp
#include <iostream>
#include <string>
using namespace std;
// 基础情况:没有参数时只输出格式字符串中的普通字符
void formatPrint(const char* fmt) {
while (*fmt) {
if (*fmt == '%' && *(fmt + 1) == '%') {
++fmt;
}
cout << *fmt;
++fmt;
}
}
// 递归处理:匹配 % 并输出对应参数
template<typename T, typename... Args>
void formatPrint(const char* fmt, T value, Args... args) {
while (*fmt) {
if (*fmt == '%') {
++fmt;
if (*fmt == '%') {
cout << '%';
++fmt;
continue;
}
// 输出当前参数
cout << value;
// 递归处理剩余参数
formatPrint(fmt + 1, args...);
return;
}
cout << *fmt;
++fmt;
}
}
// 辅助宏(或函数)
template<typename... Args>
void myPrintf(const char* fmt, Args... args) {
formatPrint(fmt, args...);
}
int main() {
myPrintf("Hello, %!\n", "world");
myPrintf("% + % = %\n", 1, 2, 3);
myPrintf("Percent sign: %%\n");
myPrintf("Mixed: % is %.2f, and %\n", 42, 3.14159, "done");
return 0;
}
输出:
text
Hello, world!
1 + 2 = 3
Percent sign: %
Mixed: 42 is 3.14, and done
八、常见错误
1. 忘记递归终止函数
cpp
// ❌ 缺少空参数版本,递归不会终止
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
cout << first;
print(rest...); // 当 rest 为空时找不到匹配
}
2. 在运行时使用 sizeof...
cpp
int n = sizeof...(args); // ✅ 编译期常量
// 但不能这样:
if (sizeof...(args) > 0) { ... } // ✅ 可以,编译期判断
3. 参数包展开顺序错误
cpp
// 初始化列表展开的顺序是确定的(从左到右)
int arr[] = { (cout << args, 0)... }; // 顺序正确
// 但函数参数的求值顺序不确定
func(func1(args...), func2(args...)); // 危险
九、这一篇的收获
你现在应该理解:
-
可变参数模板 :
template<typename... Args>,接受任意数量和类型的参数 -
sizeof...:获取参数包大小(编译期常量) -
递归展开:用终止函数 + 递归模板处理参数包
-
初始化列表展开 :利用
{ (expr, 0)... }一次性展开 -
折叠表达式 (C++17):
(args + ...)简化展开 -
tuple原理 :递归继承 + 特化实现,get<N>编译期索引
💡 小作业:实现一个
makeArray函数,接受任意多个参数,返回std::array(需要 C++17)。要求:auto arr = makeArray(1, 2, 3, 4);得到array<int, 4>{1,2,3,4}。提示:需要推导数组大小。
下一篇预告 :第44篇《typename与class的区别,依赖类型名与template消除歧义》------模板中 typename 和 class 大多数时候可以互换,但有种情况必须用 typename:告诉编译器"这是个类型"。template 关键字也有类似的消歧义作用。下篇讲清楚这些细节。