【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的典型进步。

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

相关推荐
剑锋所指,所向披靡!11 分钟前
C++之类模版
java·jvm·c++
钟离墨笺12 分钟前
Go语言--2go基础-->基本数据类型
开发语言·前端·后端·golang
小郭团队41 分钟前
1_7_五段式SVPWM (传统算法反正切+DPWM3)算法理论与 MATLAB 实现详解
开发语言·嵌入式硬件·算法·matlab·dsp开发
2501_944526421 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 蜘蛛纸牌游戏实现
android·java·python·flutter·游戏
C+-C资深大佬1 小时前
C++风格的命名转换
开发语言·c++
No0d1es1 小时前
2025年粤港澳青少年信息学创新大赛 C++小学组复赛真题
开发语言·c++
点云SLAM1 小时前
C++内存泄漏检测之手动记录法(Manual Memory Tracking)
开发语言·c++·策略模式·内存泄漏检测·c++实战·new / delete
好评1241 小时前
【C++】二叉搜索树(BST):从原理到实现
数据结构·c++·二叉树·二叉搜索树
zylyehuo1 小时前
error: no matching function for call to ‘ros::NodeHandle::param(const char [11], std::string&, const char [34])’
c++·ros1
码上成长1 小时前
JavaScript 数组合并性能优化:扩展运算符 vs concat vs 循环 push
开发语言·javascript·ecmascript