C++ 11:可变参数模板

目录

一.基本语法及原理

二.包扩展

三.emplace系列接口

一.基本语法及原理

  • 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:函数参数包 ,存放一堆函数实参 符号 ... 叫做省略号,用来标识 "包"。

区分两个概念:

  1. 模板参数包 :...Args → 一堆类型
  2. 函数参数包 :Args... args → 一堆变量

核心语法规则:

  1. sizeof...(包名) 用来获取参数包里参数的数量:

    template<class ...Args>
    void Test(Args&&... args)
    {
    cout << sizeof...(Args) << endl; // 类型个数
    cout << sizeof...(args) << endl; // 参数个数,二者相等
    }

  2. 参数包不能直接遍历 C++11:依靠递归展开 拆包 C++17:新增折叠表达式简化展开

  3. 编译器原理 可变模板不是一个能接收无限参数的单个函数;编译器根据你传入参数的数量与类型,实例化出多个不同签名的函数。

    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);

发生了什么:

  1. 字符串字面量 "11111"、"苹果1",1 作为参数包完整传入 emplace_back
  2. 通过 std::forward<Args>(args)... 包扩展 + 完美转发
  3. 直接在 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,再移动进容器,多一次开销

二、关键总结

  1. 当传入现成对象(左值 / 右值) emplace_back ≈ push_back,没有明显优势。
  2. 当直接传入构造所需参数列表 emplace_back 优势巨大:零临时对象,原地构造,这是 push_back 无法实现的写法。
  3. 底层技术栈串联你全部学过的内容: 万能引用 → 引用折叠 → 可变参数模板(参数包)→ 包扩展 → std::forward 完美转发 → emplace 原地构造。

三、面试标准问题

Q:emplace_back 和 push_back 的区别?

  1. push_back:接收已经构造完成的元素对象;
  2. emplace_back:接收构造对象所需的参数包,原地直接构造;
  3. emplace_back 可以避免产生临时对象,减少拷贝 / 移动开销;
  4. emplace_back 依靠可变参数模板、万能参数包、完美转发实现。
相关推荐
IMPYLH1 小时前
HTML 的 <th> 元素
前端·html
风骏时光牛马1 小时前
AI浪潮下程序员的职场破局与能力成长思考
前端
m0_380743871 小时前
Qt实现调用相机进行拍照并进行图像处理
开发语言·c++
IMPYLH1 小时前
HTML 的 <thead> 元素
前端·html
励志不掉头发的内向程序员3 小时前
【从零写一个CAD 03】三个 double 值得单独一个类吗:把视图变换抽成 View
开发语言·c++·qt·学习·系统架构
Doubbbbbbble云3 小时前
基于贪心算法的区间覆盖问题求解策略4
算法·贪心算法
不会就选b9 小时前
算法日常・每日刷题--<贪心>25
数据结构·算法
FungLeo9 小时前
成为全栈·React 管理后台篇·总复盘:从“接口能调通”到“后台值得使用”
前端·react.js·前端框架·项目复盘·成为全栈
蒸蒸yyyyzwd10 小时前
秋招学习笔记 day46
c++·八股