C++可变参数模板深入深剖

目录
  • 概念
  • 模板定义
  • 参数包展开
    • 递归函开
    • 逗号表达式展开
  • emplace
    • 使用方法
    • 工作原理
    • 意义
  • 总结

概念

C++11 新增一员猛将就是可变参数模板,他可以允许可变参数的函数模板和类模板来作为参数,使得参数高度泛化。

在 C++11 之前类模板和函数模板中只能包含固定数量模板参数,而且也有可变参数的概念,比如 printf 函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来并不会太简单。

模板定义

函数的可变参数模板定义方式如下:

template<class …Args>
返回类型 函数名(Args… args)
{
  //函数体
}

比如:

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

注意这里的书写格式,模板参数Args前面有省略号,代表它是一个可变模板参数, 我们把带省略号的参数称为参数包 \color{red} {我们把带省略号的参数称为参数包} 我们把带省略号的参数称为参数包,参数包里面可以包含0到 N(N≥0) 个模板参数, 而 a r g s 则是一个函数形参参数包 \color{red} {而 args 则是一个函数形参参数包} 而args则是一个函数形参参数包。

模板参数包 Args 和函数形参参数包 args 的名字可以任意指定,并不是说必须叫做 Args 和 args 。

那么现在函数传参就可以实不同类型了:

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

然后在函数模板中通过sizeof计算参数包中参数的个数:

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

现在最大的难点就是我们无法直接获取参数包中的每个参数,语法并不支持使用 args[i] 的方式来获取参数包中的参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点。

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

参数包展开

递归函开

该方法大概分为三步:

  • 给函数模板增加一个模板参数,从接收的参数包中分离出一个参数出来
  • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包
  • 继续递归,直到参数包中所有参数都被取出来

比如:

template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //继续递归调用
}

那么最后还有一个问题就是:递归展开该如何终止?

方法其实挺简单就是写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同,如果传入的参数包中参数个数是 0,那么就会匹配到这个无参递归终止函数,这样就结束了递归:

//递归终止函数
void ShowList()
{
	cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //继续递归调用
}

但是外部调用 ShowList 时不会传入参数,就会直接匹配到无参递归终止函数。而我们本意是想让外部调用 ShowList 函数时匹配到函数模板,并不是直接匹配递归终止函数。

因此我们可以将展开函数和递归调用函数的函数名改为 ShowListArg,然后重新编写一个 ShowList 函数模板,在该函数模板的函数体中要做的就是调用ShowListArg 的展开参数包 :

void ShowListArg()
{
	cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{
	cout << value << " ";
	ShowListArg(args...); //继续递归
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{
	ShowListArg(args...);
}

这样无论外部调用时传入多少个参数,最终匹配到的都是同一个函数了,那么如何编写带参的递归终止函数呢

比如带一个参数的:

template<class T>
void ShowListArg(const T& t)
{
	cout << t << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);    //继续递归
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{
	ShowListArg(args...);
}

但该方法有一个缺陷,在调用 ShowList 函数时至少要传入一个参数,否则就会报错,因为此时无论是调用递归终止函数还是展开函数,都需要至少一个参数,那我们能不能先计算一下参数包中的参数个数呢?

答案是:No!可能你会觉得 sizeof 这里也可以直接计算参数个数,来康康 错误示范 \color{red} {错误示范} 错误示范:

template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印传入的第一个参数
	if (sizeof...(args) == 0)
	{
		return;
	}
	ShowList(args...);    //继续递归
}

首先函数模板并不能调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数才能够被调用,而这个推演过程是在编译时进行的,当推演到参数包 args 中参数个数为 0 时,函数不会停下会继续推演完毕,这时就会继续传入 0 个参数时的 ShowList 函数,此时就会报错 ShowList 函数没有参数。

这里编写的 if 判断是运行时才跑的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑!

逗号表达式展开

我们知道数组可以通过列表进行初始化。如果参数包中各个参数类型都是整型,那么也可以把这个参数包放到列表中,初始化这个整型数组,此时参数包中参数就放到数组中了:

template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { args... }; //列表初始化
	//打印参数包中的各个参数
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
}

这样就可以传入多个参数了:

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

但 C++ 并不像 Python 一样激进敢秀,C++ 规定器中存储的数据类型是相同的,因此调用 ShowList 时传入的参数只能是整型,并且还不能传入 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(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc…}

//支持无参调用
void ShowList()
{
	cout << endl;
}
//处理函数
template<class T>
void PrintArg(const T& t)
{
	cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
	cout << endl;
}

当然,我们也可以不使用逗号表达式,这里的问题是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设为整型,然后用这个返回值去初始化整型数组也是可以的:

void ShowList()
{
	cout << endl;
}
//处理函数
template<class T>
int PrintArg(const T& t)//返回值为int类型
{
	cout << t << " ";
	return 0;
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { PrintArg(args)... }; //列表初始化
	cout << endl;
}

emplace

C++11 给 STL 容器增加 emplace 的插入接口,比如 list 容器的 push_front、push_back 和insert 函数,都有了对应的 emplace_front、emplace_back 和 emplace 函数:

这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:

emplace 接口的可变模板参数类型都带有KaTeX parse error: Expected '}', got '&' at position 14: \color{red} {&̲&} ,这个表示的是万能引用,而不是右值引用

使用方法

emplace 接口使用方式与容器原有的插入接口使用方式类似,但又有一些不同之处,以 list 的 emplace_back 和 push_back 为例:

调用 push_back 插入元素时,可以传入左值对象或右值对象,也可以使用列表进行初始化;调用emplace_back 插入元素时,也可以传入左值对象或右值对象,但不可以使用列表进行初始化。

除此之外,emplace系列接口最大的特点就是,插入元素可传入用于构造元素的参数包

int main()
{
	list<pair<int, string>> mylist;
	pair<int, string> kv(10, "111");
	mylist.push_back(kv);                              //左值
	mylist.push_back(pair<int, string>(20, "222"));    //右值
	mylist.push_back({ 30, "333" });                   //列表初始化

	mylist.emplace_back(kv);                           //左值
	mylist.emplace_back(pair<int, string>(40, "444")); //右值
	mylist.emplace_back(50, "555");                    //参数包
	return 0;
}

工作原理

emplace 接口先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化。

然后调用 allocator_traits::construct 函数对这块空间进行初始化,调用该函数会传入这块空间的地址和用户传入的参数,注意要完美转发;在 allocator_traits::construct 中会使用定位 new 表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数,这里同样需要完美转发

最后将初始化好的新结点插入到对应的数据结构中,比如 list 就是将新结点插入到底层的双链表中

意义

emplace 接口的可变参数模板类型都是万能引用,因此既可以接收左值,也可以接收右值,还可以接收参数包

如果调用 emplace 接口时传入的是左值,首先需要先在此之前调用构造函数实例化出一个左值对象,最后使用定位 new 表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数

如果调用 emplace 接口时传入的是右值,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数

如果调用 emplace 接口时传入的是参数包,就可以直接调用函数进行插入,并最终使用定位 new 表达式调用构造函数对空间进行初始化时,匹配到构造函数

一句话就是:

传入左值,调用构造函数+拷贝构造函数。
传入右值,调用构造函数+移动构造函数。
传入参数包,只需要调用构造函数

注意,这里前提是容器中存储的是一个需要深拷贝的类,并且该类实现了移动构造函数,否则传入左值和传入右值的效果是一样的,都会调用一次构造和一次拷贝构造

因为容器原有的 push_back、push_front 和 insert 也提供了右值引用的接口,所以 emplace 的部分功能和原有容器是重复的,如果调用时传入右值,那么最终也会调用对应的移动构造函数进行资源转移。

emplace 最大特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说 emplace 系列接口更高效的原因

但 emplace 并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么 emplace 系列接口的效率其实和原有的效率是一样的

emplace 真正高效的情况是传入参数包的时候, 直接通过参数包构造出对象,避免了中途的一次拷贝 \color{red} {直接通过参数包构造出对象,避免了中途的一次拷贝} 直接通过参数包构造出对象,避免了中途的一次拷贝

namespace cl
{
	class string
	{
	public:
		//构造函数
		string(const char* str = "")
		{
			cout << "string(const char* str) -- 构造函数" << endl;

			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1]; //开辟空间(多开一个用于存放'\0')
			strcpy(_str, str); //将C字符串拷贝到已开好的空间
		}
		//交换两个对象数据
		void swap(string& s)
		{
			std::swap(_str, s._str); //交换两个对象的C字符串
			std::swap(_size, s._size); //交换两个对象的大小
			std::swap(_capacity, s._capacity); //交换两个对象的容量
		}
		//拷贝构造函数(现代写法)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;

			string tmp(s._str); //调用构造函数,构造一个s._str的对象
			swap(tmp); //交换这两个对象
		}
		//移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
		//拷贝赋值函数(现代写法)
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;

			string tmp(s);
			swap(tmp); //交换
			return *this; //返回左值
		}
		//移动赋值
		string& operator(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
		//析构函数
		~string()
		{
			//delete[] _str;  //释放_str指向的空间
			_str = nullptr; //置空,防止非法访问
			_size = 0;
			_capacity = 0;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

这里我们用模拟实现的 string 来验证 emplace 的机制:

int main()
{
	list<pair<int, cl::string>> mylist;

	pair<int, cl::string> kv(1, "one");
	mylist.emplace_back(kv);                              //左值
	cout << endl;
	mylist.emplace_back(pair<int, cl::string>(2, "two")); //右值
	cout << endl;
	mylist.emplace_back(3, "three");                      //参数包
	return 0;
}

结果如下:

我们自己实现的 string 的拷贝构造函数复用了他的拷贝函数,所以在调用 string 的拷贝构造的时候会紧跟一次拷贝函数的调用。

当然,如果想要更加完美的体现 emplace 的作用,这里存的是 char 类型,为了体现参数包的概念,可以将 list 中更换成 pair 类型对象,这里不赘述了,有兴趣的可自行实现。

总结

到此这篇关于C++可变参数模板的文章就介绍到这了,更多相关C++可变参数模板内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++可变参数函数的实现方法示例

    C++编程中实现可变参数函数有多种途径,本文介绍一种最常见的实现途径,即可变参数宏方法:形参生命为省略符,函数实现时用参数列表宏访问参数. 1. 可变参数宏实现变参函数 可变参数宏实现可分为以下几个步骤: 函数形参原型中给出省略符: 函数实现中声明一个va_list可变参数列表变量: 开始初始化构造va_list变量: 访问变参列表: 完成清理工作: 上述步骤的实现需要使用到四个宏: va_list void va_start(va_list ap, last_arg) type va_arg

  • C++可变参数的函数与模板实例分析

    本文实例展示了C++可变参数的函数与模板的实现方法,有助于大家更好的理解可变参数的函数与模板的应用,具体内容如下: 首先,所谓可变参数指的是函数的参数个数可变,参数类型不定的函数.为了编写能处理不同数量实参的函数,C++提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型:如果实参的类型不同,我们可以编写可变参数模板.另外,C++还有一种特殊的省略符形参,可以用它传递可变数量的实参,不过这种一般只用于与C函数交互的接口程序. 一.可变参数函数

  • c++可变参数模板使用示例源码解析

    目录 前言 认识可变模板参数 使用可变模板参数 递归法 特例化 包拓展 完美转发 总结 前言 我们知道,C++模板能力很强大,比起Java泛型这种语法糖来说,简直就是降维打击.而其中,可变参数模板,就是其中一个非常重要的特性.那什么是可变参数模板,以及为什么我们需要他? 首先我们考虑一个经典的场景: 我们需要编写一个函数,来打印变量信息. 比如: int code = 1; string msg = "success"; printMsg(code,msg); // 输出: 1,suc

  • C++可变参数模板的展开方式

    文章目录 前言可变参数模板的定义参数包的展开递归函数方式展开逗号表达式展开enable_if方式展开折叠表达式展开(c++17) 总结 前言 可变参数模板(variadic templates)是C++11新增的强大的特性之一,它对模板参数进行了高度泛化,能表示0到任意个数.任意类型的参数.相比C++98/03这些类模版和函数模版中只能含固定数量模版参数的“老古董”,可变模版参数无疑是一个巨大的进步. 如果是刚接触可变参数模板可能会觉得比较抽象,使用起来会不太顺手,使用可变参数模板时通常离不开模

  • c++11可变参数使用示例

    复制代码 代码如下: #include <iostream>#include <initializer_list>using namespace std;int get_sum(int, initializer_list<int>);int main(int argc, char *argv[]){     cout << get_sum(2, {1,2,3}) << endl;    return 0;} int get_sum(int i,

  • 浅析C++模板类型中的原样转发和可变参数的实现

    目录 原样转发的意义 模板的可变参数 总结 原样转发的意义 前文我们实现了一个my_move函数,用来模拟stl的move操作,实现去引用的功能.其内部的原理就是通过remove_reference实现去引用操作. 有时我们也需要保留原类型的左值或者右值属性,进行原样转发,此时就要用forward实现转发功能. 我们先定义一个模板函数 template <typename F, typename T1, typename T2> void flip1(F f, T1 t1, T2 t2) {

  • 浅析C++可变参数模板的展开方式

    目录 前言 可变参数模板的定义 参数包的展开 递归函数方式展开 逗号表达式展开 enable_if方式展开 折叠表达式展开(c++17) 总结 前言 可变参数模板(variadic templates)是C++11新增的强大的特性之一,它对模板参数进行了高度泛化,能表示0到任意个数.任意类型的参数.相比C++98/03这些类模版和函数模版中只能含固定数量模版参数的“老古董”,可变模版参数无疑是一个巨大的进步. 如果是刚接触可变参数模板可能会觉得比较抽象,使用起来会不太顺手,使用可变参数模板时通常

  • C++11中的可变参数模板/lambda表达式

    目录 1.可变参数模板 递归函数方式展开参数包 逗号表达式展开参数包 2.lambda表达式 先来看看lambda表达式的例子: lambda表达式语法 1.可变参数模板 C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板,相比C++98和C++03,类模板和函数模板中只能含固定数量的模板参数,可变参数模板无疑是一个巨大的改进.可是可变参数模板比较抽象,因此这里只会写出够我们使用的部分. 下面是一个基本可变参数的函数模板 // Args是一个模板参数包,args是一个

  • 一篇文章让你彻底明白c++11增加的变参数模板

    目录 前言 1. 什么是变参数模板 2. 变参数模板的基础-模板形参包 2.1 非类型模板形参包 2.2 类型模板形参包 2.3 模板模板形参包 3. 模板形参包的延伸-函数形参包 4. 模板形参包的展开方法 5. stl中使用模板形参包的案例 总结 前言 本篇文章介绍一下c++11中增加的变参数模板template<typename... _Args>到底是咋回事,以及它的具体用法. 说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的. 按照惯例,还是先看一下本文大纲,如下

  • C++ 中的Lambda表达式写法

    小喵的唠叨话: 寒假之后,小喵在家里无所事事,最近用C++写代码的时候,用到了std::sort这个函数,每次用这个函数,小喵似乎都得查一下lambda表达式的写法.正好最近很闲,不如总结一下. 在Bing上搜索 C++ lambda ,第一条记录就是MSDN上的C++ lambda的介绍.本文也是基于这篇文章来写的. 那么接下来,我们分几个部分来介绍. 一.什么是Lambda表达式 MSDN上对lambda表达式的解释: 在 C++ 11 中,lambda 表达式(通常称为 "lambda&q

  • 结合C++11新特性来学习C++中lambda表达式的用法

    在 C++ 11 中,lambda 表达式(通常称为 "lambda")是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象的简便方法. Lambda 通常用于封装传递给算法或异步方法的少量代码行. 本文定义了 lambda 是什么,将 lambda 与其他编程技术进行比较,描述其优点,并提供一个基本示例. Lambda 表达式的各部分 ISO C++ 标准展示了作为第三个参数传递给 std::sort() 函数的简单 lambda: #include <algorit

  • 剖析C++中的常量表达式与省略号的相关作用

    C++ 常量表达式 常量值是指不会更改的值.C + + 提供了两个关键字,它们使你能够表达不打算修改对象的意图,还可让你实现该意图. C++ 需要常量表达式(计算结果为常量的表达式)以便声明: 数组边界 case 语句中的选择器 位域长度规范 枚举初始值设定项 常量表达式中合法的唯一操作数是: 文本 枚举常量 声明为使用常量表达式初始化的常量的值 sizeof 表达式 必须将非整型常量(显式或隐式)转换为常量表达式中合法的整型.因此,以下代码是合法的: const double Size = 1

  • Bootstrap 网站实例之单页营销网站

    我们已经掌握了很多实用 Bootstrap 的重要技能.现在,是时候拿出更多的创意来帮助客户实现他们全方位在线营销的愿望了.此次将带领大家做一个漂亮的单页高端营销网站. 主要任务如下: □ 一个大型介绍性传送带图片展示区,配有自定义的响应式欢迎信息: □ 一个客户留言区,显示为带标题的图片墙,就像砖垒的一样: □ 一个功能清单,使用大号 Font Awesome 图标: □ 一个带有自定义价目表的注册区: □ 一个带动态滚动的 ScrollSpy 导航条. 1.概况 有一位潜在客户联系我们,她深

  • 基于C++11的threadpool线程池(简洁且可以带任意多的参数)

    C++11 加入了线程库,从此告别了标准库不支持并发的历史.然而 c++ 对于多线程的支持还是比较低级,稍微高级一点的用法都需要自己去实现,譬如线程池.信号量等.线程池(thread pool)这个东西,在面试上多次被问到,一般的回答都是:"管理一个任务队列,一个线程队列,然后每次取一个任务分配给一个线程去做,循环往复." 貌似没有问题吧.但是写起程序来的时候就出问题了. 废话不多说,先上实现,然后再啰嗦.(dont talk, show me ur code !) 代码实现 #pra

随机推荐