目录
一.基本语法及原理
- C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
- 我们用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class... 或 typename... 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟... 指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
- 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这里我们可以使用 sizeof... 运算符去计算参数包中参数的个数。

可变模板参数(模板参数包) C++11 引入,使用 ... 声明,可以接收任意数量、任意类型 的模板参数,一组打包在一起的类型集合,称为参数包。
template<class ...Args> // Args 就是【模板参数包】
void Func(Args... args) // args 是【函数参数包】
{
}
class ...Args/typename ...Args:模板参数包,存放一堆类型Args... args:函数参数包 ,存放一堆函数实参 符号...叫做省略号,用来标识 "包"。
区分两个概念:
- 模板参数包 :
...Args→ 一堆类型- 函数参数包 :
Args... args→ 一堆变量
核心语法规则:
-
sizeof...(包名) 用来获取参数包里参数的数量:
template<class ...Args>
void Test(Args&&... args)
{
cout << sizeof...(Args) << endl; // 类型个数
cout << sizeof...(args) << endl; // 参数个数,二者相等
} -
参数包不能直接遍历 C++11:依靠递归展开 拆包 C++17:新增折叠表达式简化展开
-
编译器原理 可变模板不是一个能接收无限参数的单个函数;编译器根据你传入参数的数量与类型,实例化出多个不同签名的函数。
Func(1,2,"abc");
Func(10);
// 生成两份不同实例函数
| 项目 | sizeof |
sizeof... |
|---|---|---|
| 作用 | 求内存占用字节数 | 求参数包内元素数量 |
| 操作对象 | 普通类型、变量、表达式 | 模板参数包 / 函数参数包 |
| 能不能用于可变参数以外 | ✅ 随处可用 | ❌ 只能配合参数包 |
括号内能不能加 ... |
不能 | 参数包名后面不能再加... |
| 返回值含义 | 字节大小 (size_t) | 参数个数 (size_t) |

二.包扩展
1. 定义
包扩展:使用 ... 把参数包里所有元素展开,逐个铺开。 只有存在参数包(模板参数包 Args... / 函数参数包 args...)时才能进行包扩展。
编译器会把模式复制多份,包中每一个元素对应一份。
C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。


下面这种写法不对,编译会报错,为什么呢?
首先参数包是在编译时进行推导的,但这种写法是在运行时,相当于编译器走到这里会先编译出合适的类型再进行运行,但下面是运行时的过程,只有编译完才会运行,编译都没进行这就会报错
其次这是可变模板参数,传过来的数据类型都不一样,怎么能用同意容器来存放不同的数据类型

// 递归出口
void Print(){}
template<class T, class ...Args>
void Print(T first, Args... args)
{
cout << first << " ";
Print(args...); // args... 包扩展,递归调用
}
调用 Print(1,2,3)
Print(args...) → Print(2,3)
下一轮 → Print(3)
下一轮 → Print()
包扩展只有这个作用吗?下面我们讲解重要作用
三.emplace系列接口
cpp
#include<list>
// emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
int main()
{
list<bit::string> lt;
// 传左值,跟push_back一样,走拷贝构造
bit::string s1("11111111111");
lt.emplace_back(s1);
cout << "*****************************" << endl;
// 右值,跟push_back一样,走移动构造
lt.emplace_back(move(s1));
cout << "*****************************" << endl;
// 直接把构造string参数包往下传,直接用string参数包构造string
// 这里达到的效果是push_back做不到的
lt.emplace_back("1111111111");
cout << "*****************************" << endl;
list<pair<bit::string, int>> lt1;
// 跟push_back一样
// 构造pair + 拷贝/移动构造pair到list的节点中data上
pair<bit::string, int> kv("苹果", 1);
lt1.emplace_back(kv);
cout << "*****************************" << endl;
// 跟push_back一样
lt1.emplace_back(move(kv));
cout << "*****************************" << endl;
////////////////////////////////////////////////////////////////////
// 直接把构造pair参数包往下传,直接用pair参数包构造pair
// 这里达到的效果是push_back做不到的
lt1.emplace_back("苹果1", 1);
cout << "*****************************" << endl;
return 0;
}
一、分 3 种调用场景对比(重点)
场景 1:传入已经存在的对象(左值)
bit::string s1("11111");
lt.emplace_back(s1);
等价于 push_back(s1)
s1左值,T 推导为bit::string&- 原地调用 拷贝构造函数 创建容器内对象。
场景 2:传入右值(std::move)
lt.emplace_back(move(s1));
等价于 push_back(std::move(s1))
move(s1)是右值,T 推导为bit::string- 原地调用 移动构造函数。
上面两种写法,
emplace_back和push_back性能表现基本一致。
场景 3:【emplace 独有的优势】直接传入构造参数包 ⭐
// push_back 无法直接这样写!
lt.emplace_back("1111111111");
lt1.emplace_back("苹果1", 1);
发生了什么:
- 字符串字面量
"11111"、"苹果1",1作为参数包完整传入emplace_back - 通过
std::forward<Args>(args)...包扩展 + 完美转发 - 直接在 list 节点的内存上调用对应构造函数 ✅ 不需要临时对象!
和 push_back 对比
lt.push_back("1111111111");
// push_back内部行为:
// 1. 先用字面量构造一个临时 bit::string(临时对象)
// 2. 将临时对象移动构造进容器节点
// 存在一次多余临时对象构造+析构!
对于 pair<bit::string,int>:
lt1.emplace_back("苹果1", 1);
// 直接原地调用 pair 的构造 pair(const char*, int)
如果使用 push_back:
lt1.push_back(make_pair("苹果1",1));
// 需要先构造临时pair,再移动进容器,多一次开销
二、关键总结
- 当传入现成对象(左值 / 右值)
emplace_back≈push_back,没有明显优势。 - 当直接传入构造所需参数列表
emplace_back优势巨大:零临时对象,原地构造,这是 push_back 无法实现的写法。 - 底层技术栈串联你全部学过的内容: 万能引用 → 引用折叠 → 可变参数模板(参数包)→ 包扩展 → std::forward 完美转发 → emplace 原地构造。
三、面试标准问题
Q:emplace_back 和 push_back 的区别?
push_back:接收已经构造完成的元素对象;emplace_back:接收构造对象所需的参数包,原地直接构造;- emplace_back 可以避免产生临时对象,减少拷贝 / 移动开销;
- emplace_back 依靠可变参数模板、万能参数包、完美转发实现。