【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以上的代码,习惯用它。

相关推荐
JRedisX20 小时前
Java从零手写企业级内存数据库
java
萧瑟余晖20 小时前
JDK 20 新特性详解
java·开发语言
hdsoft_huge20 小时前
SpringBoot系列17:日志体系Logback配置,日志分割、脱敏、异常堆栈收集规范(生产级落地)
java·spring boot·logback
tkevinjd20 小时前
力扣148-排序链表
算法·leetcode·链表
吃饱了得干活20 小时前
扫码登录如何实现?从原理到实战
java·spring boot·redis
JRedisX20 小时前
JRedisX 项目骨架搭建指南 从零开始 — 手把手教你搭
java
zx11545020 小时前
Java 反射 (SpringBoot篇)
java·架构
ch0sen1pm20 小时前
刚学完 CAS 原子操作,我花了一晚上写了三个无锁队列
c++
qeen8720 小时前
【C++】vector的模拟实现详解(二)
c++·学习·算法·迭代器·stl
2301_7944615720 小时前
Activiti/BPMN 2.0 的 4 种网关
java·服务器·开发语言