目录
[1. 可变参数模版](#1. 可变参数模版)
[1. 1 核心概念](#1. 1 核心概念)
[1.2 常用参数传递方式](#1.2 常用参数传递方式)
[1.3 编译期核心流程](#1.3 编译期核心流程)
[1.4 核心优势](#1.4 核心优势)
[2. 包扩展](#2. 包扩展)
[2.1 常见的几种包扩展方式](#2.1 常见的几种包扩展方式)
[方式 1:递归包扩展(C++11 经典方式)](#方式 1:递归包扩展(C++11 经典方式))
[方式 2:初始化列表 + 逗号表达式(非递归方式)](#方式 2:初始化列表 + 逗号表达式(非递归方式))
[2.2 几种包扩展方式对比](#2.2 几种包扩展方式对比)
[3. 与 emplace 系列接口的关联](#3. 与 emplace 系列接口的关联)
[二. emplace系列接口](#二. emplace系列接口)
[1. 传统接口 vs emplace 接口](#1. 传统接口 vs emplace 接口)
[2. 双向链表实现中的 emplace 接口](#2. 双向链表实现中的 emplace 接口)
[3. 核心设计思想总结](#3. 核心设计思想总结)
[4. 最佳实践建议](#4. 最佳实践建议)
1. 可变参数模版
1.1 核心概念
可变参数模板的核心是参数包(Parameter Pack),分为两种:
- 模板参数包 :template <typename... Args>,
Args是类型参数包(可包含任意数量的类型)- 函数参数包 :void func(Args... args),
args是值参数包(可包含任意数量的对应类型的值)其中 ... 是参数包展开符,是可变参数模板的核心,所有操作最终都围绕「展开参数包」展开。
注意:Args 和 args 是自定义的名字(你可以写成 T.../t...),但惯例用 Args 表示类型包,args 表示值包。
1.2 常用参数传递方式
// 值传递
template <class... Args>
void Func(Args... args) {}
// 左值引用传递
template <class... Args>
void Func(Args&... args) {}
// 万能引用传递(推荐,支持完美转发)
template <class... Args>
void Func(Args&&... args) {}
1.3 编译期核心流程
代码示例:
template<class ...Args>
void Print(Args&&...args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print();
Print(1);
Print(1, string("11111111"));
Print(1, string("11111111"), x);
return 0;
}
//编译器会为每一组不同的实参类型/数量,生成独立的函数实例
//编译器实例化出以下四个函数:
void Print<>() { cout << 0 << endl; }
void Print<int>(int&& __arg1) { cout << 1 << endl; }
void Print<int, std::string>(int&& __arg1, std::string&& __arg2) { cout << 2 << endl; }
void Print<int, std::string, double&>(int&& __arg1, std::string&& __arg2, double& __arg3) { cout << 3 << endl; }
sizeof...(args)是参数个数 :它和sizeof运算符不同,专门用于统计参数包中元素的数量,是编译期常量。
以 Print(1, string("11111111"), x); 为例:
步骤1:推导模版参数包: 编译器逐个分析实参,推导 Args = <int, std::string, double&>。
- 实参 1:
1→int类型的右值 → 推导为**int**- 实参 2:
string("11111111")→std::string类型的右值 → 推导为**std::string**- 实参 3:
x→double类型的左值 → 推导为**double&** (左值会被推导为左值引用)最终,模板参数包Args被推导为:Args = <int, std::string, double&>
步骤 2:应用「引用折叠」,确定函数参数类型
函数参数是 Args&&... args,编译器会对每个 Args 类型执行引用折叠,确定最终函数参数类型:
| 推导后的 Args 类型 | Args&& 展开 | 引用折叠结果 | 最终参数类型 |
|---|---|---|---|
| int | int&& | 无折叠(纯右值) | int&& |
| std::string | std::string&& | 无折叠(纯右值) | std::string&& |
| double& | double& && | 折叠为 double& | double& |
步骤 3:生成「具体的函数实例」
编译器会根据推导结果,生成一个全新的、非模板的函数(这个函数会被编译进二进制文件):
// 编译器自动生成的实例化函数(你写的代码里看不到,但二进制里存在)
void Print<int, std::string, double&>(int&& __arg1, std::string&& __arg2, double& __arg3)
{
cout << 3 << endl; // sizeof...(args)是编译期常量,直接替换为3
}
本质:模板是代码生成器,编译器为每一组不同的实参类型 / 数量,生成独立的函数实例。
核心:编译器根据实参推导模板参数包→ 应用引用折叠 → 生成具体的非模板函数(
sizeof...直接替换为常量)。
1.4 核心优势
如果没有可变参数模版,我们需要为每一种参数数量都写一个独立模版:
- 1 个参数:
template<class T1> void Print(T1&& arg1)- 2 个参数:
template<class T1, class T2> void Print(T1&& arg1, T2&& arg2)- 3 个参数:
template<class T1, class T2, class T3> ...- ...... 无限重复,代码冗余。
可变参数模板的意义在于:
- 消除代码冗余:无需为每种参数数量写独立模板。
- 灵活性升级:让 C++ 模板从「处理固定个数的任意类型」升级为「处理任意个数的任意类型」。
2. 包扩展
2.1 常见的几种包扩展方式
方式 1:递归包扩展(C++11 经典方式)
这是 C++11 中最经典的包扩展方式,核心逻辑是「每次拆出第一个参数,剩下的参数包继续递归」,必须搭配递归终止函数(参数包为空时调用)。
代码示例(打印任意参数):
// 递归终止函数:参数包为空时调用(必须!)
void print()
{
cout << "包扩展完成" << endl;
}
// 可变参数模板函数:递归拆包
template <typename T, typename... Args>
void print(T first, Args... rest) //可根据需要写成万能引用 T&& first,Args&& ...rest
{
// 处理第一个参数(核心:只处理单个参数,不碰包)
cout << first << endl;
// 扩展剩余参数包:rest... 把剩余参数传入下一次递归
print(rest...);
}
int main()
{
print(10, 3.14, string("hello"), 'A'); // 测试任意参数
return 0;
}
底层逻辑:

//错误写法:
void print()
{
cout << "包扩展完成" << endl;
}
//编译时递归推导解析参数
template <typename T, typename... Args>
void print(T&& first, Args&&... rest)
{
if (sizeof...(args) == 0)
return;
cout << first << endl;
print(rest...);
}
核心原因:普通if是运行时判断,但参数包的递归展开是编译期行为 ,编译器会先检查
print(rest...)的合法性(rest 为空时无法匹配带参模板函数),根本到不了运行时的 if 判断,因此必须单独写无参终止函数。
总结:
递归展开的核心是逐层剥除第一个参数:每次处理一个参数,剩余参数包继续递归,直到为空。
终止函数是递归的出口:必须存在无参版本,否则递归无法停止。
所有展开逻辑在编译期完成:运行只是依次调用编译器生成的多个函数实例,无额外开销。
方式 2:初始化列表 + 逗号表达式(非递归方式)
利用 std::initializer_list 遍历参数包的特性,结合逗号表达式逐个执行操作,避免递归。
#include <iostream>
#include <string>
#include <initializer_list> // 需包含头文件
using namespace std;
template <typename... Args>
void print(Args... args)
{
// 核心:初始化列表遍历参数包,逗号表达式执行操作
(void)initializer_list<int>{
// 对每个args执行:打印 → 返回0(给初始化列表用)
(cout << args << " " , 0)...
};
}
int main()
{
print(1, 2.5, "test"); // 输出3个参数
return 0;
}
std::initializer_list是 C++11 引入的轻量级容器 std::initializer_list是 C++11 为支持{}初始化设计的只读临时值列表,核心是接收一组同类型值,基础用途是让函数 / 类支持{}初始化(如 STL 容器)
原理拆解:
initializer_list的巧妙用法------利用其"遍历同类型值"的特性,强制编译器展开参数包:
- initializer_list<int>{...}:创建一个 int 类型的初始化列表,编译器会执行每个(cout << args << " " , 0)表达式,填充列表。
- (cout << args << " " , 0):逗号表达式,先执行打印参数的逻辑,再返回0(给初始化列表提供合法的int值)。
- ...展开符:放在表达式末尾,告诉编译器对参数包args里的每一个元素,都执行一次这个逗号表达式。
- (void):避免编译器警告 "初始化列表变量未被使用"(因为我们只利用初始化列表的遍历特性,不需要使用这个列表对象),不影响功能。
总结:
- **非递归包扩展,**用初始化列表 + 逗号表达式替代递归,无需写终止函数;
- 所有展开逻辑在编译期完成,运行时仅执行打印,无额外开销;
- 是 C++11 中实现任意参数遍历的经典写法,比递归更简洁。
方式3:函数调用实现包扩展(非递归方式)
利用函数调用时的参数包扩展,把对每个参数的处理逻辑嵌入到另一个函数调用中。
template <class T>
int GetArg(const T& x)
{
cout << x << " "; // 处理单个参数:打印
return 0; // 必须返回值!否则无法作为Arguments的实参
}
// Arguments 可以是空函数它的唯一作用是 "接住" 扩展后的参数列表,让编译器完成对每个args的GetArg调用,本身不需要实现任何逻辑。
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
//GetArg(args)...; //error! C++语法不允许在独立的表达式语句后使用包展开,...展开操作必须发生在"可接受参数列表的地方"
Arguments(GetArg(args)...); //对参数包args中的每一个元素,都先调用GetArg(单个参数),再把所有GetArg的返回值作为参数包,传入Arguments函数。
}
int main()
{
double x = 2.2;
Print(1, string("11111111"), x);
return 0;
}
执行流程
调用Print(1, string("11111111"), x);时,编译器会把GetArg(args)...展开为:
Arguments(GetArg(1),GetArg("11111111"),GetArg(x));
执行顺序:
- 依次调用GetArg(1) → 打印1,返回0
- 依次调用GetArg("11111111") → 打印11111111,返回0
- 依次调用GetArg(x) → 打印2.2,返回0
- 最后调用Arguments(0, 0, 0),但这个函数是空的,什么都不做。
与方式2对比:
- 本质相同:都是在编译期把参数包拆成单个元素,逐个执行处理逻辑。
- 区别只是载体不同:
初始化列表+逗号表达式:用initializer_list<int>{......}承载拓展;
函数调用:用Arguments(...)函数调用承载扩展。
总结:
- 用
GetArg处理单个参数;- 用
Arguments(...)作为载体,把GetArg(args)...扩展成完整的参数列表;- 最终效果:遍历参数包,对每个参数执行
GetArg逻辑,实现和递归展开一样的功能,但代码更简洁、无递归终止函数。
2.2 几种包扩展方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 递归包扩展 | 直观易懂,符合递归思维 | 必须写终止函数,代码稍繁琐 | 适合理解包扩展原理 |
| 初始化列表 + 逗号表达式 | 代码简洁,无终止函数 | 依赖 initializer_list,逗号表达式可读性稍弱 |
适合快速实现简单遍历操作 |
| 函数调用式包扩展 | 灵活可扩展,无终止函数 | 需要额外载体函数,理解成本稍高 | 适合封装复杂参数处理逻辑 |
3. 与 emplace 系列接口的关联
可变参数模板是 emplace 系列接口的核心技术基础:
emplace_back(Args&&... args)利用可变参数模板接收构造参数。- 通过
std::forward<Args>(args)...完美转发参数,在容器内存中就地构造元素,避免临时对象的拷贝 / 移动。
二. emplace系列接口
emplace系列是C++11引入的容器接口(如emplace_back / emplace_front / emplace),核心优势是就地构造元素 ,避免临时对象的创建和拷贝/移动,大幅提升性能(尤其对拷贝成本高的类型,如std::string/自定义大对象)
1. 传统接口 vs emplace 接口
| 特性 | 传统接口(push_back/push_front) | emplace 系列接口 |
|---|---|---|
| 构造方式 | 先创建临时对象 → 拷贝 / 移动到容器 → 销毁临时对象 | 直接在容器的内存空间里构造对象 |
| 开销 | 至少 1 次拷贝 / 移动 + 1 次析构 | 0 次拷贝 / 移动,仅 1 次构造 |
| 设计初衷 | 传入已构造好的对象 | 传入构造参数,最大化减少拷贝/移动 |
2. 双向链表实现中的 emplace 接口
以双向链表为例,完善实现emplace_back / emplace_front / emplace 接口。
#include <iostream>
using namespace std;
namespace pig
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
// C++11 可变参数模板构造
template <class... Args>
list_node(Args&&... args)
:_data(std::forward<Args>(args)...)
, _next(nullptr)
, _prev(nullptr)
{}
};
//迭代器省略......
template<class T>
class list
{
private:
typedef list_node<T> Node;
Node* _head;
size_t _size;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
const_iterator end()const
{
return const_iterator(_head);
}
//空初始化
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
//默认构造
list()
{
empty_init();
}
list(initializer_list<T> il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
// 拷贝构造
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
// C++11 移动构造
list(list<T>&& lt) noexcept
:_head(lt._head)
, _size(lt._size)
{
// 置空原链表,避免析构时重复释放
lt._head = nullptr;
lt._size = 0;
}
// 拷贝赋值
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
// C++11 移动赋值
list<T>& operator=(list<T>&& lt) noexcept
{
swap(lt);
return *this;
}
//析构函数
~list()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
cout << "链表已销毁" << endl;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
//prev cur next
prev->_next = next;
next->_prev = prev;
delete cur;
--_size;
return next;
}
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x){insert(end(), std::move(x));} // C++11 右值版本push_back
void push_front(const T& x){insert(begin(), x);}
void push_front(T&& x){insert(begin(), std::move(x));}// C++11 右值版本push_front
iterator insert(iterator pos, const T& val)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(val);//先拷贝构造Node的_data
newnode->_next = cur;
newnode->_prev = prev;
prev->_next = newnode;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
// C++11 右值版本insert
iterator insert(iterator pos, T&& val)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(std::move(val)); // 先移动构造Node的_data
newnode->_next = cur;
newnode->_prev = prev;
prev->_next = newnode;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
// C++11 emplace核心:就地构造节点(无需先构造T对象)
template <typename... Args>
iterator emplace(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
// 直接用构造参数构造Node的_data,无拷贝/移动
Node* newnode = new Node(std::forward<Args>(args)...);
newnode->_next = cur;
newnode->_prev = prev;
prev->_next = newnode;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
// C++11 emplace_back:尾插
template <typename... Args>
void emplace_back(Args&&... args)
{
emplace(end(), std::forward<Args>(args)...);
}
// C++11 emplace_front:头插
template <typename... Args>
void emplace_front(Args&&... args)
{
emplace(begin(), std::forward<Args>(args)...);
}
};
}
自定义 string 类(模拟高拷贝成本类型):
namespace cat
{
class string
{
public:
//默认构造
string(const char* str = "")
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "构造" << endl;
if (str == nullptr)
{
_str = new char[1];
*_str = '\0';
}
else
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
}
//拷贝构造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "拷贝构造" << endl;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
//移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "移动构造" << endl;
swap(s);
}
//拷贝赋值
string& operator=(const string& s)
{
if (this != &s)
{
char* new_str = new char[s._capacity + 1];
strcpy(new_str, s._str);
delete[] _str;
_str = new_str;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
//移动赋值
string& operator=(string&& s)
{
cout << "移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
//cout << "析构" << endl;
delete[] _str;
_str = nullptr;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
测试代码:
#include"List.h"
int main()
{
pig::list<cat::string> lt;
cat::string s1("111111111"); //构造
cat::string s2("222222222"); //构造
cout << "******************" << endl;
//传左值:都走拷贝构造
lt.push_back(s1);
lt.emplace_back(s1);
cout << "******************" << endl;
//传右值:都走移动构造
lt.push_back(move(s1));
lt.emplace_back(move(s2));
cout << "******************" << endl;
//直接传构造参数:
//单个参数:
lt.push_back("11111111"); //走隐式类型转换 (构造+移动构造)
lt.emplace_back("11111111"); //构造
cout << "******************" << endl;
//多个参数:
pig::list<pair<cat::string, int>> lt2;
//lt2.push_back("苹果", 5);//不支持这样写!
lt2.push_back({ "苹果", 5 });//走隐式类型转换(构造 + 移动构造)
lt2.emplace_back("苹果", 5); //构造 直接用参数包构造pair
cout << "******************" << endl;
return 0;
}
3. 核心设计思想总结
- 完美转发 :
std::forward<Args>(args)...保证参数以原始值类别(左值 / 右值)传递给T的构造函数,避免额外拷贝。- 就地构造 :直接在容器分配的节点内存中构造
T对象,无需先创建临时对象再拷贝 / 移动。- 接口复用 :
emplace_back/emplace_front复用emplace,代码简洁且易维护。- 性能优势 :
- 对单参数构造 :
emplace避免临时对象创建,比push_back少 1 次构造 + 1 次移动。- 对多参数构造 :
emplace是唯一支持直接传参的方式,push_back必须先构造临时对象。
4. 最佳实践建议
- 优先使用
emplace_back/emplace_front,尤其是:
- 元素类型拷贝 / 移动成本高(如自定义大对象、
std::string)- 需要直接传递构造参数(尤其是多参数场景)
- 当需要传递已构造好的左值 / 右值对象 时,
push_back和emplace_back性能差异不大,可根据可读性选择。