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

相关推荐
豆沙沙包?30 分钟前
C++~~~vector容器(p31-P40)
开发语言·c++
小小de风呀31 分钟前
de风——【从零开始学习C++】(十八):AVL树——让二叉搜索树自己“站起来“的自平衡黑科技
c++·科技·学习
原野池予40 分钟前
深入Java集合框架:数组与List互转底层原理剖析(JDK 8)
java·数据结构·list
牛油果子哥q1 小时前
C++STL容器详解:vector/list/string底层扩容、迭代器失效终极解决、容器选型避坑
开发语言·c++·list
Java成神之路-1 小时前
深入理解 HashMap 扩容时机:JDK 7 vs JDK 8 的设计差异与取舍
java·juc
李冰然1 小时前
深入Java集合框架:数组与List互转底层原理剖析(JDK 8)
java
竹枝溪1 小时前
MySQL进阶:约束、多表设计、多表查询与事务
java·数据库·mysql·事务·子查询·acid·多表查询
上玄code1 小时前
【Agent精讲】调一个LLMAPI背后的工程问题
java·开发语言·python·chatgpt
李冰然1 小时前
深入Java集合框架:ArrayList源码解析(JDK 8)
java
welfare9621 小时前
新号别搞:c++类与对象
开发语言·c++