【C++】可变参数模板与emplace系列

模板一直是C++泛型编程的基石,但C++98的模板参数个数是固定的。碰到参数个数不确定的场景,只能写一堆重载,或者用宏。C++11引入了可变参数模板,把类型和个数的泛化同时解决了。

目录

[1. 基本语法](#1. 基本语法)

[2. 包扩展](#2. 包扩展)

[3. emplace系列:比push_back更进一步](#3. emplace系列:比push_back更进一步)


1. 基本语法

class ...Args定义模板参数包,Args... args定义函数参数包。

cpp

复制代码
template <class ...Args>
void Print(Args&&... args) {
    cout << sizeof...(args) << endl;  // 包中参数个数
}

调用时,编译器会根据实参实例化出对应个数和类型的函数:

cpp

复制代码
Print();                         // 实例化 void Print();
Print(1);                        // 实例化 void Print(int&&);
Print(1, string("hello"));       // 实例化 void Print(int&&, string&&);

本质上,可变参数模板还是一对一的实例化,只是帮我们把重复劳动交给了编译器。

2. 包扩展

参数包本身不能像数组一样遍历,能用它做的事情几乎只有一件事:扩展

最常见的展开方式是递归地取第一个参数,把剩余包继续往下传:

cpp

复制代码
// 递归终止:参数包为空
void ShowList() {
    cout << endl;
}

template <class T, class ...Args>
void ShowList(T x, Args... args) {
    cout << x << " ";
    ShowList(args...);  // 包扩展:把剩余参数展开传给下一层
}

template <class ...Args>
void Print(Args... args) {
    ShowList(args...);
}

编译时递归展开的过程:

text

复制代码
Print(1, string("hello"), 2.2);
 → ShowList(1, string("hello"), 2.2);
   → cout << 1 << " "; ShowList(string("hello"), 2.2);
     → cout << "hello" << " "; ShowList(2.2);
       → cout << 2.2 << " "; ShowList();  // 终止

还有一种更"现代"的写法,把参数包直接作为实参交给另一个函数:

cpp

复制代码
template <class T>
const T& GetArg(const T& x) {
    cout << x << " ";
    return x;
}

template <class ...Args>
void Arguments(Args... args) {}

template <class ...Args>
void Print(Args... args) {
    Arguments(GetArg(args)...);  
    // 包扩展为:Arguments(GetArg(x1), GetArg(x2), ...)
}

这个写法抽象但有美感:GetArg(args)...在每个参数上调用GetArg,再把结果打包传给Arguments

3. emplace系列:比push_back更进一步

C++11给STL容器新增了emplace_backemplace系列接口。它们也是可变参数模板,使用方式上兼容push_backinsert,但多了一个关键能力:直接用构造对象的参数,在容器内部原地构造

cpp

复制代码
list<demo::string> lt;
demo::string s1("hello");
lt.emplace_back(s1);                 // 拷贝
lt.emplace_back(move(s1));           // 移动
lt.emplace_back("hello world");      // 直接用const char*构造,零拷贝

最后一行是push_back做不到的。push_back("hello world")会先用const char*构造一个临时string对象,再移动插入;而emplace_back直接把"hello world"作为参数包传下去,在容器节点的数据对象上进行一次构造。少了一次临时对象的创建和析构。

对于pair这类多参数对象,优势更明显:

cpp

复制代码
list<pair<demo::string, int>> lt1;
lt1.emplace_back("apple", 1);  // 直接在节点内构造pair,无需拼接pair对象

实现原理也不复杂。以list为例:

cpp

复制代码
template <class... Args>
void emplace_back(Args&&... args) {
    insert(end(), forward<Args>(args)...);  // 完美转发参数包
}

template <class... Args>
iterator insert(iterator pos, Args&&... args) {
    Node* newnode = new Node(forward<Args>(args)...);  // 构造节点时转发
    // ... 链接节点 ...
}

节点构造函数也是可变参数模板:

cpp

复制代码
template <class... Args>
ListNode(Args&&... args)
    : _next(nullptr), _prev(nullptr),
      _data(forward<Args>(args)...)  // 转发给_data的构造
{}

一环扣一环,参数包被完美转发到最终存储位置。中途没有临时对象,也没有多余的移动或拷贝。这就是emplace系列的效率来源。

结合完美转发传递参数包 是emplace实现的关键。展开参数包时需要写成forward<Args>(args)...,这样才能保留每个参数的原始值类别。如果忘了forward,参数包中的右值到达节点构造时就会变成左值,退化成拷贝。

在实际工程中,emplace_back已经基本可以取代push_back,可读性不降,效率有提升的可能。写C++11以上的代码,习惯用它。

相关推荐
一切皆是因缘际会4 小时前
AI Agent落地困局与突破:从技术架构到企业解析
数据结构·人工智能·算法·架构
sheeta19984 小时前
LeetCode 每日一题笔记 日期:2026.05.16 题目:154. 寻找旋转排序数组中的最小值 II
笔记·算法·leetcode
计算机安禾4 小时前
【c++面向对象编程】第28篇:new/delete vs malloc/free:C++中正确动态内存管理
开发语言·c++·算法
qeen874 小时前
【算法笔记】各种常见排序算法详细解析(下)
c语言·数据结构·c++·笔记·学习·算法·排序算法
逐光老顽童4 小时前
Java 内存模型深度解析与 JVM 调优实战指南
java·架构
写了20年代码的老程序员4 小时前
Excel 导入导出为什么总是把后端逼成字段搬运工
java·excel
ChoSeitaku4 小时前
10.枚举_Record_密封类_debug_API文档_Object类_lombok_Junit
java·数据库·junit
zhoumeina994 小时前
如何保证不同位置切换合成底图的渲染顺序
java·前端·javascript
欢璃4 小时前
笔试强训练习
java·开发语言·jvm·数据结构·算法·贪心算法·动态规划