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

C++11特性:可变参数模板

前言

可变参数在大家学习C语言之初,写下第一个打印”Hello World“的程序时就接触过了,即printf()。第二个参数 ... ,就是可变参数。

就使用而言,可变参数允许我们一次性传入多个参数,并且类型不限。

但上述可变参数尽针对函数参数,

在C++11中,为了完善泛型编程,引入了可变参数模板,使得模板参数也具备了此特性,能够接受数个不同类型的模板参数。

可变参数模板

语法

形式很简单,只要在模板参数前,带上 ... 就行。

template<class ...Args>  //typename也可;省略号跟在class后也行

...Args就是包含各个参数的参数包,可看作一个形似数组的容器,可以存0个或n个参数,不限类型。(形似数组理解,不是数组是因为数组是相同类型元素的集合)

...Args可以作为函数模板参数包,也可作为类模板参数包。

 比较

C的可变参数是通过4个宏实现的,且如果定义了一个带可变参数的函数,还必须一起传入可变参数的个数。

但C++的可变模板参数不同,...Args参数包可以直接接收任意个数的任意类型的参数。基础原理就仰仗于函数重载和模板。

原理

这里要先介绍一下sizeof...()操作符,其可计算参数包的参数个数。

 为了方便理解其原理,这里介绍函数模板的参数包。

template<class ...Args>
void Print(Args&&... args) {cout << sizeof...(args) << endl;
}int main()
{int x = 1;Print();Print(x);Print(x,2.5);Print(x, 2.5, "Hello");return 0;
}

上文提到其原理仰仗于函数重载和模板

其实就是,模板使用参数,推导出不同参数列表的函数重载。

上面的代码本质如下:

void Print();void Print(int&& x);void Print(int&& x, double&& y);void Print(int&& x, double&& y, string&& z);

但我们还可以想象,如果没有可变模板参数,我们要让编译器推出上面的函数,

就得写下面1个函数 + 3个函数模板

void Print();template<class T>
void Print(T&& t);template<class T1, class T2>
void Print(T1&& t1, T2&& t2);template<class T1, class T2, class T3>
void Print(T1&& t1, T2&& t2, T3&& t3);

可幸亏有了可变模板参数,在类型泛化的基础上叠加了数量变化,使得泛型编程更加灵活。 

包拓展

 但到目前为止,我们对参数包有了一定了解,可是如何从参数包中获取参数并使用呢?

sizeof...()操作符远远不够,我们还需要了解包拓展。

void ShowList() {}template<class T, class ...Args>
void ShowList(T t, Args... args)
{cout << t << " ";ShowList(args...);
}template<class ...Args>
void Print(Args... args) {ShowList(args...);
}int main()
{int x = 1;Print(x, 2.5, "Hello");return 0;
}

 

以上就是包拓展的过程,本质就是用模板帮我们把参数包解构,用 1 + n-1 的函数模板重载 n 个函数,但是要自行处理没有参数的函数重载。

但实际上,这个玩法已经过时了,在C++17中已经有了更高级的玩法,感兴趣可以去看看文档,问问AI。

不过其实还有一种玩法,只不过有点抽象,需要体会。。

template<class T>
const T& getArgs(const T& t)
{cout << t << " ";return t;
}template<class ...Args>
void Arguments(Args... args)
{}template<class ...Args>
void Print(Args... args) {Arguments(getArgs(args)...);//用getArgs的返回值构成Arguments()的参数包
}int main()
{int x = 1;Print(x, 2.5, "Hello");return 0;
}

本质如下


void Print(int x, string y, double z)
{Arguments(getArgs(z), getArgs(y), getArgs(x));
}

怎么是倒序?C++规定是这样的,参数包展开时的求值顺序是从右到左的。 规则是别人定的。

要正序其实还是离不开上一种形似递归(bushi)的方法。

应用(emplace系列)

C++11给STL容器都新增了一个接口emplace

瞩目的点当然就是这个可变参数模板了,怎么好端端的push_back,insert不够香吗?

接下来会以list的emplace_back进行解释。

对比emplace与push系列

一般在定义list时,我们其实都已经确定好了模板参数,如下。

list<string> lt;

那么在push_back时,该函数模板也被实例化为

void push_back(const string& val);lt.push_back("666");

 如上场景中,"666"是一个const char*型的常量字符串,

就需要先隐式类型转换,构造一个string临时对象,再将临时对象拷贝构造给结点数据域的string,最后尾插。

看起来没什么,请看如下场景:

template <class... Args>void emplace_back (Args&&... args);lt.emplace_back("666");

同样是const char*的常量字符串,push_back()由于实例化好了,要进行隐式转换

而emplac_back()只有在调用、接受参数后,编译器才会实例化它,并且参数类型是const char*!

这有差别吗?有的,有的。

有了 const char* 的常量字符串,可以直接再将其传给结点的可变参数模板的构造函数,此时又可以继续将const char* 常量字符串作为参数去直接构造结点的string成员变量,走string(const char* s)的构造,只需要构造一次就够了!

好了,我知道这段话很长很臭,请看下图:

 图中实现了一路将常量字符串传递给了结点string的构造,只要进行一次构造就够了。

而相对于push_back(),还要不断地拷贝构造,这个效率在对比下就体现出来了。

(补充:emplace_back的底层可以理解为又去复用了一个可变参数模板的insert(),中间要用完美转发传递右值参数)

template <class... Args>
void emplace_back(Args&&... args)
{insert(end(), std::forward<Args>(args)...);//!!!
}template <class... Args>
iterator insert(iterator pos, Args&&... args)
{Node* cur = pos._node;Node* newnode = new Node(std::forward<Args>(args)...);//!!!Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);
}

但上述是否能证明emplace系列就一定比push系列、insert()高效呢?

NO。

如果是如下情形,

string s("666");//左值传参
lt.push_back(s);lt.emplace_back(s);//右值传参
lt.push_back((move)s);lt.emplace_back((move)s);

 此时二者的效率就没差别了,都是字符串,要么拷贝构造、要么移动拷贝。

总结

总结一下,emplace系列在接受到构造函数所需参数时,能够直接构造,中间不需要拷贝构造。如此情形下,效率较高;而其他情形的效率没差。(读者可以用list<pair<string, int>>自行感受)

但毕竟emplace存在过人之处,哪怕只有一个,也是过人之处。

所以以后还是推荐用emplace系列。

上述涉及”移动语义“与”右值“的前置知识,建议学完再看emplace部分。。。

----------------------------------------------------------完--------------------------------------------------------------------

相关文章:

  • npm/yarn/pnpm安装时Sharp模块报错解决方法
  • Debian 系统 Python 开发全解析:从环境搭建到项目实战
  • 域控账号密码抓取
  • Debian 11之解决daemon.log与syslog文件占用空间过大问题
  • Spring Boot微服务架构(六):伪装的微服务有哪些问题?
  • 微服务及容器化设计--可扩展的架构设计
  • Vue组件技术全解析大纲
  • vue3 getcurrentinstance 用法
  • Ubuntu实现和主机的复制粘贴 VMware-Tools(open-vm-tools)
  • ​扣子Coze飞书多维表插件-查询数据
  • [图文]图6.3会计事项-Fowler分析模式的剖析和实现
  • WebSocket学习总结
  • MySQL数据库第一章
  • Introduction to SQL
  • SQLord: 基于反向数据生成和任务拆解的 Text-to-SQL 企业落地方案
  • sqli_labs第二十九/三十/三十一关——hpp注入
  • 【手写数据库核心揭秘系列】第9节 可重入的SQL解析器,不断解析Structure Query Language,语言翻译好帮手
  • [STM32学习笔记(九)]CubeMX项目使用系统定时器SysTick的中断服务函数进行定时
  • 《计算机组成原理》第 1 章 - 计算机系统概论
  • DAY38打卡
  • 什么网站做任务可以赚钱/如何制作小程序
  • wordpress 仿36氪主题/武汉网站建设优化
  • 全国各地网站开发外包/直接进入网站的代码
  • dedecms 网站 经常无法连接/seo推广策略
  • 西安网站建设咪豆/谷歌浏览器在线打开
  • 宁夏建设网站的公司电话/企业seo排名哪家好