当前位置: 首页 > news >正文

可变参数模板

引入:为什么会有可变参数模板?

        在C++98/03中,也就是我们之前学的类模版和函数模版中只能含固定数量的模版参数,而C++11中新增的可变参数模板能够不固定数量的模版参数!

一:可变参数模板的格式

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}//随意更换
template <class ...>
void ShowList(T... t)
{}

解释:

①:模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N ( N ≥ 0 ) N(N\geq 0)N(N≥0)个模板参数,而args则是一个函数形参参数包。

②:Args译为参数,我们之前一般用T来表示模板,这里的Args也可以随便更换,但用Args和args是最好的,和官方一致

现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。比如:

int main()
{ShowList();ShowList(1);ShowList(1, 'A');ShowList(1, 'A', string("hello"));return 0;
}

但是,使用不是重点,可变参数模版的重点在于如何获取参数包里面的每一个参数...

二:错误展开参数包

当main中传入参数如下的时候:

ShowList(1, 'A', string("hello"));

我们希望能得到传入的参数,并将其打印出来,期望效果如下:

错误方法->期望循环遍历数组打印

template<class ...Args>
void ShowList(Args... args)
{//错误示例:for (int i = 0; i < sizeof...(args); i++){cout << args[i] << " "; //打印参数包中的每个参数}cout << endl;
}

报错如下:

解释:以上操作是运行时解析,而展开参数包需要编译时解析

换句话说:在C++中,模板参数包的展开必须在编译时完成,而以上代码试图在运行时通过循环访问参数包,这是不允许的。

编译和运行两者的区别:

编译时解析(Compile-Time Expansion)

含义:模板和参数包的展开是在代码编译期间完成的,编译器需要知道所有参数的类型和数量。

运行时解析(Run-Time Access)

含义:在程序运行期间动态访问数据(如数组、循环等),此时模板参数已经编译成具体代码,无法再改变。

  • sizeof...(args) 可以获取参数包的大小(编译时计算),但 args[i] 试图在运行时访问参数包,这是不允许的

  • 原因

    • 参数包 args 不是数组或容器,它是一组编译时确定的参数,无法在运行时动态索引。

    • 模板在编译时实例化,例如 ShowList(1, "hello", 3.14) 会生成一个专门的函数,但 args[1] 这样的操作在语法上不存在。

三:正确展开参数包

①:递归展开参数包

void _ShowList()
{cout << endl;
}//如果想依次拿到每个参数类型和值,编译时递归解析
template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{cout << val << " ";_ShowList(args...);
}template <class ...Args>
void ShowList(Args... args)
{_ShowList(args...);
}int main()
{ShowList(1, 'A', string("hello"));return 0;
}

代码解释:

a:入口函数->就是我们正常写的可变参数模版

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

_ShowList(args...);代表展开参数包进行传递给  _ShowList函数,  _ShowList现在的参数就是(1,'A',"hello"),而  _ShowList的参数就是能够接受可变模板参数的,所以匹配

解释:只需调用 ShowList(1, "abc", 3.14),无需关心递归逻辑。

b:递归展开

template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{cout << val << " ";  // 处理当前参数_ShowList(args...);  // 递归处理剩余参数
}
  • 作用:每次调用时,分离出参数包的第一个参数val),并递归处理剩余参数(args...)。

  • 关键点

    • T:当前参数的类型(如 intstring 等)。

    • val:参数包的第一个值(如 1"hello" 等)。

    • Args... args:剩余的参数包(数量比当前少 1)。

    • 递归调用_ShowList(args...) 会继续匹配这个模板或终止条件。

c:递归终止条件

void _ShowList()
{cout << endl;
}
  • 作用:当参数包 args... 被递归展开到时,调用这个无参版本,结束递归。

  • 关键点

    • 这是一个普通函数(非模板),用于终止递归。

    • 当 args... 为空时,_ShowList() 会匹配这个版本,打印换行并结束调用链。

运行结果:

递归过程的展开讲解:

//实例化以后,推演生成的过程
void ShowList(int val1, char ch, std::string s)
{_ShowList(val1, ch, s);
}void _ShowList(const int& val, char ch, std::string s)
{cout << val << " ";_ShowList(ch, s);
}void _ShowList(const char& val, std::string s)
{cout << val << " ";_ShowList(s);
}void _ShowList(const std::string& val)
{cout << val << " ";_ShowList();
}

解释:在递归的过程中,参数会越来越少,最后会调用无参的_ShowList终止循环

②:逗号表达式展开参数包

这个方法很抽象,在理解这个方法之前,要先学会一些东西

语法1:在 C++ 中,当在初始化的括号中进行{args...} ,这一步能直接展开参数包!

语法2:逗号表达式的性质

语法1列子:args遇到int数组初始化的{ }

//展开函数
template<class ...Args>
void ShowList(Args... args)
{int arr[] = { args... }; //数组初始化//打印参数包中的各个参数for (auto e : arr){cout << e << " ";}cout << endl;
}int main()
{ShowList(1,2,3,4);return 0;
}

运行结果:

解释:在 C++ 中,当在初始化的括号中进行{args...} ,这一步能直接展开参数包;题目中用的是数组的{},所以会展开参数包并逐个填充数组。

类似:

int arr[] = { args... }; 参数包展开后如下:↓int arr[] = { 1,2,3,4};

也就是说,当参数包全是整形,或者参数是能够转换为整形的值的时候(double...),展开参数包,直接用一个数组接收然后遍历数组打印就可以了!

但是问题是,参数包一般不会全是整形,有可能如下(int char string都在一起):

(1, 'A', string("sort")

这个时候,无法用一个int数组的{}来展开并接受所有的参数了 ,会报错

(字符转换为整形的ascll码值了)

所以现在我们思路是:

a:既然{}中能够进行参数包的打开,那我们能不能运用这个特性在{}中将展开得到的参数逐个的打印?

b:但是只在数组的{}中进行参数包的展开是肯定不行的,人家数组接收的是int,那有什么东西即能在{}中展开参数包,又能返回一个整形值?

c:所以我们用一个逗号表达式操作,逗号表达式的第一个动作先进行参数包的第一个参数的打印,第二个动作给上一个0,根据逗号表达式的特性,会执行完单个参数的打印后,返回这个0给数组,这样我们既白嫖了参数包在{}中能展开的特性,又把逗号表达式返回的0给了数组

语法1+语法2的例子:

template <class T>
void PrintArg(T t)
{cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args...), 0) };cout << endl;
}
int main()
{ShowList(1);ShowList('A');ShowList(string("hhhh"));return 0;
}

运行结果:

但是我们的参数包一般不会只是一个参数,所以要写作下面这样:

//打印参数包的单个参数
template <class T>
void PrintArg(T t)
{cout << t << " ";
}//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}int main()
{ShowList(1, 'A', string("sort"));return 0;
}

 运行结果:

代码解释:

int arr[] = { (PrintArg(args), 0)... };

这里的三个点... 一定要写在逗号表达式的外面寓意着,参数包中的每个参数都会单独的进入逗号表达式 如下所示:

{ (PrintArg(1), 0), (PrintArg('A'), 0), (PrintArg("sort"), 0) }变成↓
int arr[] ={0,0,0};

这样我们既使用{}的特性完成了参数包的展开打印,也利用了逗号表达式的特性返回了0给数组

而数组是什么值,我们根本不关心,我们关心的是展开参数包成功了

总结:数组的{}有着展开参数包的功能,但是呢数组不能白白给你展开参数包,它的{}要的是一个整形值,所以我们就用逗号表达式来即进行了展开参数包,又返回了整形值

四:可变模版参数的运用场景

可变模版参数的运用场景就是在于STL容器中的emplace相关接口函数,因为emplace相关接口函数的参数就是一个可变模版参数,这也是为什么很多人说emplace相关接口函数的效率更高的原因

例子:

如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:

比如list容器的emplace_back函数的声明如下:

注意: 

好,下面我们举例验证为什么emplace_back的效率比push_back高~

为什么说emplce接口比普通的函数接口效率更高?以list容器的emplace_back和push_back为例:

解释:emplce的参数传入参数包的时候,效率极高

        下面,我们将用库中的链表进行push_back插入和emplace_back插入一个string类型的值,而这个string类呢,我们用自己实现的,因为自己实现的string类当中的构造/拷贝构造/移动构造被调用的时候,会打印相关的信息,这样我们就知道为什么emplace_back的效率高了!

自己实现的string类代码:

namespace bit
{class string{public://构造函数string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "构造函数" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造 -- 左值string(const string& s):_str(nullptr){cout << "拷贝构造 -- 深拷贝" << endl;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}// 移动构造 -- 右值string(string&& s){cout << "移动构造" << endl;swap(s);}// 拷贝赋值 -- 左值// s2 = tmpstring& operator=(const string& s){cout << "拷贝赋值 -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}// 移动赋值 -- 右值string& operator=(string&& s){cout << "移动赋值" << endl;swap(s);return *this;}//析构函数~string(){delete[] _str;_str = nullptr;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};
}

①:对push_back和emplace_back插入对象

int main()
{std::list<bit::string> lt1;bit::string s1("xxxx");lt1.push_back(s1);lt1.push_back(move(s1));cout << "=============================================" << endl;bit::string s2("xxxx");lt1.emplace_back(s2);lt1.emplace_back(move(s2));cout << "=============================================" << endl;return 0;
}	

运行结果:

结论:当emplace参数不是参数包的时候,其效率和push_back一致!

解释:push_back:打印效果里面的构造函数是"xxxx"构造出了左值s1,然后此时链表push_back函数会对这个左值s1进行拷贝构造(深拷贝)出一个链表所需的节点;而对于move后的s1是右值,所以push_back函数会对这个右值进行移动构造出一个链表所需的节点,这就是为什么是:构造函数+拷贝构造+移动构造;enplace同理,不再赘述~

如图:

②:对push_back和emplace_back插入构造参数

int main()
{std::list<bit::string> lt1;lt1.push_back("xxxx");lt1.emplace_back("xxxx");cout << "=============================================" << endl;return 0;}

运行结果:

结论:当emplace参数是构造参数(参数包)的时候,其效率高于push_back

解释:push_back:首先会对右值"xxxx"进行构造出一个临时变量,然后push_back函数会对这个临时变量的资源进行移动构造出一个节点,所以是构造函数+移动构造(临时变量也是右值)

emplce_back呢:仅仅只需要构造函数,也就是直接用"xxxx"这个右值构造出了一个节点,所以只有构造函数

如图:

Q:为什么emplace_back()仅需要调用一次string类的构造函数,就能构造出一个链表的节点?

A:emplace_back 是 C++11 引入的高效方法,其核心特点是 直接在容器内构造对象,避免了临时对象的创建和移动/拷贝。

所以到这里,我们证明为什么emplace类的接口函数效率高,因为直接传参数包的时候,会直接构造!

下面看一个复杂场景下,emplace效率会更高:

int main()
{std::list<pair<bit::string, bit::string>> lt2;pair<bit::string, bit::string> kv1("xxxx", "yyyy");lt2.push_back(kv1);lt2.push_back(move(kv1));cout << "=============================================" << endl;pair<bit::string, bit::string> kv2("xxxx", "yyyy");lt2.emplace_back(kv2);lt2.emplace_back(move(kv2));cout << "=============================================" << endl;return 0;}

运行结果:

和前面一致,当不是传递参数包的时候,效率和push_bcak一致

当我们emplace传递参数包的时候:

int main()
{std::list<pair<bit::string, bit::string>> lt2;lt2.emplace_back("xxxx", "yyyy");cout << "=============================================" << endl;return 0;
}

运行结果:

解释:

既然你的链表存储的类型是pair<string,string>,那emplace直接用"xxxx"和"yyyy"去构造节点!

所以两次构造即可,而不传参数包需要两次构造+两次拷贝构造 或 两次构造+两次移动构造

五:emplace_bcak 和 push_back在特定情况下的差异

  • 对于浅拷贝的类(比如只包含基本类型成员,拷贝成本低),emplace_back 的优势可能不明显。

  • 但对于没有移动构造函数的类,emplace_back 能避免一次拷贝构造,提升效率。

  • 如果类有移动构造函数,push_back 和 emplace_back 的性能差距会缩小(因为临时对象可以通过移动构造转移资源,而非拷贝)。

总结:当类有移动构造的时候,我emplace_back只能省略移动构造的消耗,所以差距不大;

但当类没有移动构造,只有拷贝构造的时候,我emplace_back就及其优秀了,会省略拷贝构造!

例子:

一个没有移动构造的Date类

class Date
{
public:Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "构造函数" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "拷贝函数" << endl;}
private:int _year = 1;int _month = 1;int _day = 1;
};
int main()
{std::list<Date> lt1;lt1.push_back({ 2024,3,30 });lt1.emplace_back(2024, 3, 30);return 0;
}

运行结果:

 

解释:

六:所有源码

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;#include<vector>
#include<list>
#include<map>
#include<set>
#include<string>#include<assert.h>namespace bit
{class string{public://构造函数string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "构造函数" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// 交换函数void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "拷贝构造 -- 深拷贝" << endl;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}//移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "移动构造" << endl;swap(s);}//赋值重载//s1 = s3 string& operator=(const string& s) {if (this != &s) {char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;// 把容量和大小赋过去_size = s._size;_capacity = s._capacity;}cout << "赋值重载----深拷贝" << endl;return *this;   // 结果返回*this}// 移动赋值string& operator=(string&& s){cout << "移动赋值" << endl;swap(s);return *this;}//析构~string(){delete[] _str;_str = nullptr;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};}//体现有名对象 匿名对象 emplace都无区别 
//直接给插入对象参数的时候 才有用
//int main()
//{
//	/*std::list<bit::string> lt1;*/
//
//	//bit::string s1("xxxx");
//	//lt1.push_back(s1);
//	//lt1.push_back(move(s1));
//	//cout << "=============================================" << endl;
//
//	//bit::string s2("xxxx");
//	//lt1.emplace_back(s2);
//	//lt1.emplace_back(move(s2));
//	//cout << "=============================================" << endl;
//
//	//lt1.push_back("xxxx");
//	//lt1.emplace_back("xxxx");
//	//cout << "=============================================" << endl;
//
//	std::list<pair<bit::string, bit::string>> lt2;
//	//pair<bit::string, bit::string> kv1("xxxx", "yyyy");
//	//lt2.push_back(kv1);
//	//lt2.push_back(move(kv1));
//	//cout << "=============================================" << endl;
//
//	//pair<bit::string, bit::string> kv2("xxxx", "yyyy");
//	//lt2.emplace_back(kv2);
//	//lt2.emplace_back(move(kv2));
//	//cout << "=============================================" << endl;
//
//	lt2.emplace_back("xxxx", "yyyy");
//	cout << "=============================================" << endl;
//
//
//	return 0;
//}class Date
{
public:Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "构造函数" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "拷贝函数" << endl;}
private:int _year = 1;int _month = 1;int _day = 1;
};int main()
{std::list<Date> lt1;lt1.push_back({ 2024,3,30 });lt1.emplace_back(2024, 3, 30);cout << endl;Date d1(2023, 1, 1);lt1.push_back(d1);lt1.emplace_back(d1);cout << endl;lt1.push_back(Date(2023, 1, 1));lt1.emplace_back(Date(2023, 1, 1));return 0;
}

http://www.xdnf.cn/news/395011.html

相关文章:

  • 微服务架构-限流、熔断
  • 小智AI机器人 - 代码框架梳理2
  • 【GPT入门】第38课 RAG评估指标概述
  • 什么是深度神经网络
  • AI自动化测试工具有哪些?
  • 优秀的流程图设计软件【留存】
  • stm32实战项目:无刷驱动
  • 深入浅出之STL源码分析7_模版实例化与全特化
  • 封装和分用(网络原理)
  • C# 方法(方法重载)
  • 查看YOLO版本的三种方法
  • 关于解决MySQL的常见问题
  • Linux基础开发工具一(yum/apt ,vim)
  • 滑动窗口——将x减到0的最小操作数
  • Python中的标识、相等性与别名:深入理解对象引用机制
  • Gartner 《2025大数据管理规划指南》学习心得
  • 【安装配置教程】ubuntu安装配置Kodbox
  • 【RP2350】香瓜树莓派RP2350之搭建开发环境(windows)
  • AI日报 - 2024年05月12日
  • redis数据结构-05 (LPUSH、RPUSH、LPOP、RPOP)
  • 第二十二节:图像金字塔-拉普拉斯金字塔
  • 深入浅出:Spring Boot 中 RestTemplate 的完整使用指南
  • AI Agent(9):企业应用场景
  • springboot3+vue3融合项目实战-大事件文章管理系统-更新用户头像
  • MySQL(8)什么是主键和外键?
  • Ubuntu 22虚拟机【网络故障】快速解决指南
  • Linux:44线程互斥lesson32
  • 【言语】刷题1
  • 手机当电脑播放器 soundwire
  • Python异常处理全解析:从基础到高级应用实战