【c++进阶】c++11的魔法:从模板到可变模板.

关注我,学习c++不迷路:

个人主页:爱装代码的小瓶子

专栏如下:

  1. c++学习
  2. Linux学习

后续会更新更多有趣的小知识,关注我带你遨游知识世界

期待你的关注。


文章目录

  • [1. 前言:](#1. 前言:)
  • [2. 主要内容介绍:](#2. 主要内容介绍:)
    • [2-1 什么是参数包?](#2-1 什么是参数包?)
    • [2-2 举个小栗子:](#2-2 举个小栗子:)
    • [2-3 sizeof计算包中参数个数:](#2-3 sizeof计算包中参数个数:)
      • [2-3-1 如何理解...的位置:](#2-3-1 如何理解...的位置:)
      • [2-3-2 如何理解答案相同:](#2-3-2 如何理解答案相同:)
    • [2-4 包扩展:](#2-4 包扩展:)
    • [2-5 实际应用,emplace接口系列:](#2-5 实际应用,emplace接口系列:)
  • [3. 总结:](#3. 总结:)

1. 前言:

本文主要初步讲解什么是可变模板参数,为什么c++11需要引进可变模板参数,如何理解可变模板参数。

废话不多,我们来给出可变模板参数的概念,一句话:

在 C++ 中,可变模板参数(variadic templates)就是"可以接受任意数量参数"的模板参数。

(之前的模板不香吗,的确很好用。但是我每次传入不同个数的模板,你不就炸了吗,你这得写好多个模板,很显然,这是不合适的,因此我们引入了可变模板参数:你无论传入几个模板,我都可以稳稳的接住)

怎么写,长什么样?

cpp 复制代码
template<class ...Args>
void func(Args ...args)
{
	//
}

这是一种写法,其中class可以换成typename,大体结构保持这样。接下来我们详细解释一下这些东西,其中Args并不是一定要这样写,只是这样写的是一种不成文的规矩,我还是推荐这样写的,这是因为便于理解。

cpp 复制代码
template<class ...T>//这里Arg也可以变成其他的,比如T
void func(T ...args)
{
	//函数体
}

他其实类似与模板,甚至写法上面你也可以看出,他真的很像很类似,但是不同的是它可以接受不用的参数。接下来我们一起看看吧


2. 主要内容介绍:

2-1 什么是参数包?

参数包"就是把"很多个参数打包成一个整体的名字",你可以对这一整包做统一操作,然后再在需要的地方把这一包拆开(展开)成一个一个具体的参数。这里的参数包指的是:T ... args,这个概念其实很简单了。函数参数包(function parameter pack):对应的函数参数那一包值。

可能还是不清楚,我们来对比一下模板参数和参数包:

  • 模板参数:只能代表"一个类型"或"一个值",例如:
    template 中的 T 只代表一个类型(比如 int、double)。
  • 参数包:可以代表"一串类型 / 一串值",例如:
    template<typename... Ts> 中的 Ts 可能表示 {int, double, std::string} 这样的一堆类型。

这么来说相应地,函数参数里对应的"包"就是一串参数:void f(Ts... ts); 中的 ts 就是一包参数。所以,"参数包"就是一个"代表多个参数的包的名字"。

在这里我们理解了,什么是参数包,对于后面的例子就可以看的很清楚了。

2-2 举个小栗子:

这个可变模板参数是在c++11中引进的新技术,这里有什么作用呢?我来举个小例子:比如我想打印一组不同类型的数据,但是不用cout,此时我们可以尝试实现一个可变参数的函数

cpp 复制代码
// 1. 终止条件:没有参数时
void print()
{
}

// 2. 至少一个参数
template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest)
{
    std::cout << first << std::endl; // 处理第一个
    print(rest...);                   // 把剩下那一包展开继续递归
}



int main()
{
    print(1, 1.2, 13, "111111");
    return 0;
}

来看结果:

发现一切打印正常完成,此时完美的打印了每一个数据。如果不使用这个技巧,可能会导致写很多函数。有了这个他会自动帮你生成。同时我们发现这个调用很类似与递归的调用,最后有一个简单的递归终止条件。这里的 Rest 是一个模板参数包,rest 是函数参数包;rest... 表示把"剩下的那包参数全部展开传给下一层"。

2-3 sizeof计算包中参数个数:

  • sizeof...(Args) -> 计算类型包 Args 有多少个类型。
  • sizeof...(args) -> 计算函数参数包 args 有多少个参数。

我们继续深究参数包,我们发现,我们可以使用sizeof...来完成对包的参数的个数进行统计:

cpp 复制代码
template <class ...Args>
void print(Args ...args)
{
    cout << sizeof...(args) << endl;
    
}



int main()
{
    print(1, 1.2, 13, "111111");
    return 0;
}

我们发现两个参数是一样的,明明说args是看有几个参数类型的,Args是有几个类型的。这是为什么呢?后面在细细解答:

这里就完成了对模板参数的统计,需要注意的是... 需要放在指定的位置,才能编译通过完成运行。

2-3-1 如何理解...的位置:

在这里我们准确的来说,不是通过sizeof来完成对包的参数解析,而是sizeof...来完成对每一个参数的统计。

  • sizeof... 是一个特定的操作符(operator),就像 sizeof、decltype 一样。
  • 它后面的 ... 是操作符名字的一部分,专门用来表示"我要查的是参数包的大小",而不是"我要展开参数"。
  • 这种写法是 C++ 标准规定的"固定语法",为了区别于普通的 sizeof 和普通的参数展开 args...。

sizeof...(新朋友,C++11 引入)

作用:计算参数包里有多少个参数(返回值是 std::size_t 类型)。

写法:

  • sizeof...(Args) -> 计算类型包 Args 有多少个类型。
  • sizeof...(args) -> 计算函数参数包 args 有多少个参数。

2-3-2 如何理解答案相同:

先给你直接结论:

  1. 在数量上:

    sizeof...(Args) 和 sizeof...(args) 的结果永远是一样的。

    因为你传入了多少个值,编译器就推导出了多少个类型,它们是一一对应的。

  2. 结果依然一样。

    即使你传入两个 int,在编译器眼里,这依然是"两个参数",而不是"一个参数"。它不会因为类型相同就合并。

我们先来看一段程序:

cpp 复制代码
template<typename... Args>
void func(Args... args)
{
    std::cout << "Types count: " << sizeof...(Args) << std::endl;
    std::cout << "Args count: " << sizeof...(args) << std::endl;
}


int main()
{
    func(1,2);
    return 0;
}

这里的运行结构依旧是一致的。在这里我们就需要讲到的他的原理了:

  1. 推导 Args(类型包):
    • 第一个参数 10 是 int。
    • 第二个参数 20 也是 int。
    • 注意: C++ 编译器不会把它们合并成一个 int。它老老实实地记录为 int, int。

所以,Args = {int, int}。长度是 2。

  1. 匹配 args(值包):
    • 对应上面的类型,参数列表变成 (int arg1, int arg2)。
    • 具体值为 10, 20。

所以,args = {10, 20}。长度是 2。

你可能会问:"既然都是 int,为什么不把 Args 推导成 int,而把 args 推导成 int 的数组或者列表呢?"这是为了保持位置的对应关系。如果合并了,你就无法区分第一个 int 和第二个 int 分别对应什么逻辑了。

2-4 包扩展:

我们除了能够完成对包里面的参数完成个数统计,还可以包拓展,那什么是包拓展呢:"包扩展"(pack expansion)就是:在参数包名字后面写上 ...,让编译器把这个"一包参数"展开成一串独立的东西(类型、表达式、参数......)包扩展是一个包含"参数包 + 省略号"的结构,表示把这个参数包展开成一串模式(pattern)。

cpp 复制代码
template<typename... Args>
void func(Args... args);  // Args... args 就是包扩展

这里的就是模板参数包Args...完成拓展。这个包里的参数一个一个的拿出来,完成包拓展。这里 Args... 扩展成一串类型,比如 int, double, std::string,从而 args 也变成一串参数 (int a1, double a2, std::string a3)

有这个过后,我们可以看下面这个例子:

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

int main()
{
	print("1111", 1, 3, 4, 6,1.2,1.8);
	return 0;
}

这个函数也很好的解释了什么是包拓展,还有函数的底部是怎么调用的,下面听我一一道来:

第一部分:代码逻辑详解:

这段代码其实设计成了一个"三明治"结构: 最底层:showList() ------ "刹车"

中间层次:也是整个代码的核心:就是void showList(T x,Args... args),这个完成对包的每次取出一个,后面再成为一个包,后续语句只需在进行包拓展就行了,这样是为什么可以完成继续调用。最顶层:print(Args... args) ------ "传话筒",不是逻辑实现的关键。

第二部分:编译器在干什么?

编译器在编译这段代码时,会像生成"俄罗斯套娃"一样,自动生成多个版本的 showList 函数:入口:编译器看到 print 被调用,生成 print 实例,里面调用了 showList。

  1. 第1次生成:
    参数:const char*, int, int, int, int, double, double (共7个)
    编译器生成 showList<const char*, int, int, int, int, double, double>
    内部逻辑:打印 "1111",然后调用下一层。
  2. 第2次生成:
    参数:int, int, int, int, double, double (剩6个)
    编译器生成 showList<int, int, int, int, double, double>
    内部逻辑:打印 1,然后调用下一层。
  3. 第3次生成:
    参数:int, int, int, double, double (剩5个)
    ...生成 showList<...>,打印 3...
  4. ... (以此类推,每层少一个参数) ...
  5. 倒数第2次生成:
    参数:double (剩1个)
    编译器生成 showList
    内部逻辑:打印 1.8,然后调用下一层。注意!此时调用的是 showList(),也就是没有参数的那个版本。
  6. 最后匹配:
    编译器发现没有参数了,直接匹配到非模板函数 void showList()。

这个就解释了为什么什么是包拓展,什么是包,是怎么调用的:

2-5 实际应用,emplace接口系列:

它们的设计目的是:"在容器内部直接构造元素",而不是"在外部构造好再拷贝/移动进去"。这点比push或者c++11之前的接口要现代许多,这也是c++这个语言一直保持很高的生命力的典型:

核心实现套路就是:

  • 声明为可变模板函数:template<class... Args>
  • 参数用"转发引用"接收:Args&&... args
  • 内部使用完美转发转发给构造函数:T(std::forward(args)...)
    (之前已经讲过什么是万能引用,什么是完美转发,详细可以去我的主页去找,也可以问AI哦)
cpp 复制代码
template<class... Args>
reference emplace_back(Args&&... args);

template<class... Args>:这就是"可变模板"------可以接受 0 个或多个不同类型的参数。

Args&&... args:这是"转发引用"形式的"函数参数包",配合 std::forward 实现完美转发。(不会改变左右值的属性,继续往下一层)

这里也不详细讲了,这个接口系列是比insert和push系列高效一点。

emplace 系列通常比 push / insert "高效",根本原因是:

emplace 在容器内部的内存上"直接构造"对象,而 push / insert 一般需要"先构造出一个对象,再拷贝/移动进去",这就多了一次构造或者一次移动的开销


3. 总结:

c++11引入了可变模板参数,极大的节省了我们写模板函数的时间,很大的方便了程序员。是c++11的典型进步。

点个关注吧,大家新年快乐。

相关推荐
java1234_小锋1 分钟前
Java高频面试题:MyBatis如何实现动态数据源切换?
java·开发语言·mybatis
knighthood20015 分钟前
Qt5.15+VTK9.3.0实现点云点选功能
开发语言·qt
墨神谕10 分钟前
Java中,为什么要将.java文件编译成,class文件,而不是直接将.java编译成机器码
java·开发语言
菜鸟国国21 分钟前
还在为 Compose 屏幕适配发愁?一个 Density 搞定所有机型!
android
卡尔特斯22 分钟前
Android Studio 代理配置指南
android·前端·android studio
sunbofiy2328 分钟前
去掉安卓的“读取已安装应用列表”,隐私合规
android
cch891831 分钟前
DCATAdmin后台框架极速上手
android
和小潘一起学AI1 小时前
CentOS 7安装Anaconda
开发语言·python
Ehtan_Zheng1 小时前
ActivityMetricsLogger 深度剖析:系统如何追踪启动耗时
android
努力努力再努力dyx1 小时前
【无标题】
开发语言·python