目录

1、可变参数模板的概念

2、参数包的展开方式

递归函数方式展开参数包

逗号表达式展开参数包

3、STL容器中的empalce相关接口函数


1、可变参数模板的概念

  • C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止。

下面就是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
  • 上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。
  • 模板参数包Args和函数形参参数包args的名称可以自己进指定。

现在调用ShowList函数就可以传入任意个数的任意类型的参数了:

int main()
{ShowList(1, 'x', 2.2, "abc");ShowList(-1, -2, -3);return 0;
}

我们也可以通过sizeof获得参数包的个数,但注意格式:sizeof...(args)

template <class ...Args>
void ShowList(Args... args)
{cout << sizeof...(args) << endl;//获取参数包中参数的个数
}

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。语法并不支持使用args[i]这样方式获取可变参数。也不支持auto范围for的方式获取可变参数:

template <class ...Args>
void ShowList(Args... args)
{/*不支持args[i]for (size_t i = 0; i < sizeof...(args); ++i){cout << args[i] << endl;}*//*不支持auto范围forfor (auto& e : args){cout << e << endl;}*/
}

由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。


2、参数包的展开方式

递归函数方式展开参数包

递归展开参数包需要实现两个函数,且二者同名:

  • 一、递归函数
  • 二、递归终止函数

我们分开来讨论:

一、递归函数:

  1. 给可变参数的函数模板增加一个模板参数,用于后续获得每一个参数的值
  2. 在该函数模板中递归调用函数模板,把剩下的参数包传进去
  3. 一直递归下去,每次分离参数包中的一个参数,直至全部分离出来

示例:

//递归函数
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{cout << sizeof...(args) << endl;//获取参数包中参数的个数cout << val << "->" << typeid(val).name() << endl;ShowList(args...);
}

写好了递归函数,接下来完成终止函数,

二、递归终止函数:

这里我们也给出两个终止函数的方式:

  1. 带参的终止函数
  2. 无参的终止函数

先看带参的终止函数,结合递归函数和测试用例一起看:

//带参的终止函数
template <class T>
void ShowList(const T& val)
{cout << val << "->" << typeid(val).name() << " end" << endl;
}

根据参数的最匹配原则,当参数包只有一个参数的时候,编译器会优先匹配到此终止函数完成递归终止。接下来把整体的代码加上测试用例一起看:

//带参的终止函数
template <class T>
void ShowList(const T& val)
{cout << val << "->" << typeid(val).name() << " end" << endl;
}
//递归函数
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{cout << sizeof...(args) << endl;//获取参数包中参数的个数cout << val << "->" << typeid(val).name() << endl;ShowList(args...);
}
int main()
{ShowList(1, 'x', 1.1);return 0;
}

  • 首先,1传给val,把x和1.1传给参数包,推出T的类型为int,参数包的个数为2,打印后,继续递归把x传给val,把1.1传给参数包,推出T的类型为char,参数表的个数为1,打印后再继续递归,此时参数包的个数只有一个,根据模板的最匹配原则,这一个参数会匹配到递归终止函数,
  • 但该方法有一个弊端就是,我们在调用ShowList函数时必须至少传入一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。

下面来看看无参的递归终止函数:

//无参的终止函数
void ShowList()
{}

此时当参数包的个数为0个的时候,就会走此函数,完成递归终止。

  • 如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
  • 而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数。

结合测试用例一起看:

//无参的终止函数
void ShowList()
{}
//递归函数
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{cout << sizeof...(args) << endl;//获取参数包中参数的个数cout << val << "->" << typeid(val).name() << endl;ShowList(args...);
}
int main()
{ShowList(1, 'x', 1.1);cout << endl;ShowList(1, 2, 3, 4, 5);cout << endl;ShowList();return 0;
}

注意:递归终止的方式不能按照如下的方式写:

//递归函数
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{//错误的写法:if (sizeof...(args) == 0){return;}cout << val << "->" << typeid(val).name() << endl;ShowList(args...);
}
  • 函数模板并不能调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
  • 而这个推演过程是在编译时进行的,当推演到参数包args中参数个数为0时,还需要将当前函数推演完毕,这时就会继续推演传入0个参数时的ShowList函数,此时就会产生报错,因为ShowList函数要求至少传入一个参数。
  • 这里编写的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。

逗号表达式展开参数包

这里我们先给出使用逗号表达式展开参数包的一个例子:

template <class T>
void PrintArg(const T& t)
{cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{//列表初始化+逗号表达式int arr[] = { (PrintArg(args), 0)... };cout << endl;
}
int main()
{ShowList(1, 'x', 1.1, string("hello world"));ShowList(1, 2, 3, 4, 5);return 0;
}

下面我将给出其演化的过程:

前面我们学习到了可以使用{ }列表初始化来初始化数组等内置类型和自定义类型,那么我可不可以直接把参数包放到列表初始化呢?

template <class ...Args>
void ShowList(Args... args)
{//列表初始化int arr[] = { args... };cout << endl;
}

这里很明显是不可以的,C++只允许数组里面是同一种类型,但是模板的可变参数就意味着我参数包的类型并不统一,会出现一会是int,一会是char……。为了解决此问题,我们可以单独封装一层函数(PrintArg),此函数专门用于获得参数包的每个数据并输出,但是这又会出现一个问题,我得不到一个返回值放回数组里头,为了解决返回值的问题,又使用了逗号表达式来解决:

  • 逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回。
  • 将逗号表达式的最后一个表达式设置为一个整型值,确保逗号表达式返回的是一个整型值。
  • 将处理参数包中参数的动作封装成一个函数,将该函数的调用作为逗号表达式的第一个表达式。

这里我们把逗号表达式的最后一个值设为0,此时我参数包里有几个参数,那么就有几个0,也就代表有几个值。调整后的代码如下:

template <class T>
void PrintArg(const T& t)
{cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{//列表初始化+逗号表达式int arr[] = { (PrintArg(args), 0)... };cout << endl;
}

注意:

  • 可变参数的省略号需要加在逗号表达式外面,表示需要将逗号表达式展开,如果将省略号加在args的后面,那么参数包将会被展开后全部传入PrintArg函数,代码中的{(PrintArg(args), 0)...}将会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc...}。

此时我们就会发现,就和一开始我们给出的代码一致了,这就是是用来逗号表达式的方式展开参数包。下面给出测试用例:

int main()
{ShowList(1, 'x', 1.1, string("hello world"));ShowList(1, 2, 3, 4, 5);return 0;
}

当然,这里其实不用逗号表达式也可以,直接给PrintArg函数带上返回值即可完成逗号表达式的功能:

template <class T>
int PrintArg(const T& t)
{cout << t << " ";return 0;
}
template <class ...Args>
void ShowList(Args... args)
{//列表初始化int arr[] = { PrintArg(args)... };cout << endl;
}

此时可以传入多种类型的参数了,但是不能不传参数,因为数组的大小不能为0,为了支持不传参数,我们需要单独写个无参的ShowList函数,就像无参版的终止函数那样:

//支持无参调用
void ShowList()
{cout << endl;
}
template <class T>
int PrintArg(const T& t)
{cout << t << " ";return 0;
}
template <class ...Args>
void ShowList(Args... args)
{//列表初始化int arr[] = { PrintArg(args)... };cout << endl;
}

总结:

  • 这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, Printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
  • expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0),(printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

3、STL容器中的empalce相关接口函数

  • http://www.cplusplus.com/reference/vector/vector/emplace_back/
  • http://www.cplusplus.com/reference/list/list/emplace_back/
template <class... Args>
void emplace_back (Args&&... args);

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和
emplace系列接口的优势到底在哪里呢?

int main()
{std::list< std::pair<int, char> > mylist;// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别mylist.emplace_back(10, 'a');mylist.emplace_back(20, 'b');mylist.emplace_back(make_pair(30, 'c'));mylist.push_back(make_pair(40, 'd'));mylist.push_back({ 50, 'e' });for (auto e : mylist)cout << e.first << ":" << e.second << endl;return 0;
}
int main()
{// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back// 是先构造,再移动构造,其实也还好。std::list< std::pair<int, cpp::string> > mylist;mylist.emplace_back(10, "sort");mylist.emplace_back(make_pair(20, "sort"));mylist.push_back(make_pair(30, "sort"));mylist.push_back({ 40, "sort" });return 0;
}

总结:

  • emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
  • 但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是一样的。
  • emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

其实我这里对emplace的讲解还不够深刻,具体可以看看龙哥(2021dragon)的博文,个人觉着他总结的确实全面,且通俗易懂,下面附上链接:2021dragon--》可变参数模板精讲

【 C++11 】可变参数模板相关推荐

  1. C++11新特性之 可变参数模板

    C++ 11的可变模版参数是其新增的最强大的特性之一.通过对参数进行了泛化,可以表示从0到任意个数.任意类型的参数.我们知道对于一个模板类来说,通常只能含固定数量的模版参数,可变模版参数无疑是一个巨大 ...

  2. [C++11新特性](24)列表初始化,右值引用,可变参数模板,lambda表达式,包装器

    文章目录 列表初始化 {}初始化 initializer_list auto.nullptr.范围for decltype STL的变化 右值引用 简介 移动构造与移动赋值 完美转发 新的类功能 可变 ...

  3. 代码模板在哪里_C++的可变参数模板

    背景 一切都从函数传参开始说起.我们知道,在C语言中有个神奇的函数:printf: printf("%s : %dn","gemfield number",70 ...

  4. C++11 可变参数

    C++11的可变参数模板,对参数进行了高度的泛化,可以表示 任意数目 任意类型 语法为 class或者typename后面加- Template<class ... T>void func ...

  5. C++11可变数量模板参数可变类型模板参数并使用lamada函数调用使用范例

    为了完成这个功能,耗费一整天. 背景是需要到一张表中查询,条件不一样,但是都可以通过PreparedStatement_setXX设置,想体验一把C++11的高级模板特性,设计如下封装 inline ...

  6. 求变量的数据类型,typeid,bool,C和C++的不同,new和delete,C++中的枚举,inline和可变参数模板,auto和函数模板,宽字符

    求变量的数据类型,通过函数typeid(变量名).name();获得变量的数据类型. 案例如下: #include<iostream> #include<stdlib.h> v ...

  7. 省略号和可变参数模板

    1.基本概念 省略号在C/C++中有很多用途,包括函数的变量参数列表.C运行库的printf()就是常见示例. 可变参数模板提供了类型安全和灵活性,可应用于类模板和函数模板. 2.语法示例 templ ...

  8. C++ Primer 5th笔记(chap 16 模板和泛型编程)可变参数模板

    1. 可变参数模板( variadic template) 一个接受可变数目参数的模板函数或模板类. 1.1 参数包 (parameter packet) 可变数目的参数被称为参数包,存在两种参数包: ...

  9. C++ 可变参数模板

    可变参数模板 C++11 增强了模板功能,在C++11之前,类模板和函数模板只能含有固定数量的模板参数,现在C++11中的新特性可变参数模板允许模板定义中包含0到任意个模板参数,可变参数模板和普通模板 ...

最新文章

  1. 《JavaScript高级程序设计(第3版)》教程大纲
  2. 如何在Simulink中添加延迟环节
  3. 什么叫返回路径平面上的间隙_信号完整性:关于走线的参考平面问题探讨
  4. HDUOJ----剪花布条
  5. 【笔记】跨域重定向中使用Ajax(XHR请求)导致跨域失败
  6. C/C++与内存相关的函数
  7. 面试官 | Class.forName 和 ClassLoader 有什么区别?
  8. C语言结构体练习-互动粒子仿真
  9. cdh 安装_使用Cloudera的CDH部署Hadoop:第二步,安装JDK
  10. 【linux】安装python依赖库confluent_kafka
  11. 机器学习之KNN 算法
  12. 深圳两所新大学,来了!
  13. CSS CSS3 pdf 电子书大全 百度云
  14. 程序员加薪升职之全路径解析
  15. 焦虑症应该怎么办?这六个缓解方法建议试试
  16. Matlab学习(台大郭彦甫)第5节-初阶绘图
  17. Vue2(十一):脚手架配置代理、github案例、插槽
  18. jquery 立体走马灯_jquery实现页面百叶窗走马灯式翻滚显示效果的方法
  19. 搜狐云景客户端工具评测之WordPress的搭建
  20. 数据库安全防护措施之防黑客攻击

热门文章

  1. 【GRE/MGRE】
  2. H3C交换机开启web管理
  3. 本体技术视点 | 基于区块链的物联网(IoT)数据管理框架
  4. 计算机中的总线仲裁方式
  5. 机器学习周志华--没有免费的午餐定理
  6. 慎用安卓USB调试模式 谨防陷入安全危机
  7. EditText插入QQ表情源码
  8. Zabbix监控进程CPU及内存
  9. 电子商务研究方向 - 综述篇
  10. 【阿冈建议】三强还是谢幕,历娜你都可以这么唱!