当前位置:   article > 正文

C++11_可变参数模板_必须在此上下文中扩展参数包

必须在此上下文中扩展参数包

概述

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
下面就是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包(两个名字可以随便起)
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
  • 1
  • 2
  • 3
  • 4
  • 5

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

如果想获得参数的个数,可以使用

sizeof…(args);

template<class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	ShowList(1, 'x', 1.1);
	ShowList(1, 2, 3, 4);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

那么怎么获取传入的参数呢?

我们试试用数组的方式:

void ShowList(Args... args)
{
	for (size_t i = 0; i < sizeof...(args); i++)
	{
		cout << args[i];
	}
}
int main()
{
	ShowList(1, 'x', 1.1);
	ShowList(1, 2, 3, 4);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

error C3520: “args”: 必须在此上下文中扩展参数包

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

递归函数方式展开参数包

// 递归终止函数
template<class T>
void ShowList(const T& val)
{
	cout << val << "-->" << typeid(val).name() << endl;
    cout<<"end"<<endl;
}
// 展开函数
template<class T, class ...Args>
void ShowList(const T& val, Args... args)
{
	cout << val << "-->" << typeid(val).name() << endl;
	ShowList(args...);
}

int main()
{
	ShowList(1, 'x', 1.1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 当所有参数传入函数void ShowList(const T& val, Args... args)之后,第一个参数被val获取,剩余参数放到args中;

  • 通过val可以访问第一个参数

  • 然后把参数包args再次传入当前函数,递归调用自己,

  • 此时args的第一个参数又被val获取,新的args又少了一个参数

  • 像这样一直取,直到args只剩下一个参数,

  • 此时再调用ShowList,就会调void ShowList(const T& val)这个重载的函数(因为只有一个参数的调用与这个匹配度更高),访问完最后一个就递归结束了。

或者也可以这样写:

// 递归终止函数
void ShowList()
{}
//展开函数
template<class T, class ...Args>
void ShowList(const T& val, Args... args)
{
	cout << val << "-->" << typeid(val).name() << endl;
	ShowList(args...);
}
int main()
{
	ShowList(1, 'x', 1.1);
	//ShowList(1, 2, 3, 4);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

最后匹配一个参数为空的调用。

那么可以使用如下的方式吗?

template<class T, class ...Args>
void ShowList(const T& val, Args... args)
{
	cout << val << "-->" << typeid(val).name() << endl;
	if (sizeof...(args) == 0)
	{
		return;
	}
	ShowList(args...);
}
int main()
{
	ShowList(1, 'x', 1.1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

error C2672: “ShowList”: 未找到匹配的重载函数

error C2780: “void ShowList(const T &, Args…)”: 应输入 2 个参数,却提供了 0 个

显然不可以

因为既然要接收多种类型的函数,那这个函数一定是重载了多个的,模板实例化的过程不是发生在运行期间的,而是编译过程中就要产生,所以必须显式重载一个接收结尾参数类型的函数。

借助列表初始化展开参数包

STL的vector中有一个emplace_back函数,可以看到,它()中只有一个参数包,没有我们上面用于接收值的T val

所以我们换一种方式展开函数包:

template <class T>
int PrintArg(T val)
{
	cout << val << "-->" << typeid(val).name() << endl;
	return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	//列表初始化
	int arr[] = { PrintArg(args)... };
	cout << endl;
}
int main()
{
	ShowList(1, 'A', std::string("sort"));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这种展开参数包的方式,不需要通过递归终止函数,

是直接在ShowList函数体中展开的, PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

这种就地展开参数包的方式实现的关键是列表初始化

先看如下的使用:

template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { args... };
	for (auto e : arr)
	{
		cout << e << " ";
	}
}
int main()
{
	ShowList(1,2,3);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

int arr[] = { args... };可以把参数包中的一个一个参数取出,用于初始化数组

这个使用方法的局限性就是只能传int类型的参数

所以我们给它进化一下

int arr[] = { PrintArg(args)… };

这个过程变成了两步:

  • 先给把函数包的第一个参数传给PrintArg(在编译期间已经根据不同类型的参数实例化了函数)
  • 再把函数的返回值传给数组

这里的数组就作为一个逐个取值的工具人,只不过每一个取出的值都被传到了对应的处理函数中,而且处理函数的返回值也仅仅是为了返回给数组,把戏演完。

也可以再进化一下(但没多大必要)

我们可以使用一下逗号表达式,让处理函数的返回值可以是void

void PrintArg(T val)
{
	cout << val << "-->" << typeid(val).name() << endl;
}
  • 1
  • 2
  • 3
  • 4

int arr[] = { (PrintArg(args),0)… };

一个逗号表达式会从前向后执行,并返回最后一个的返回值,

这里用都好表达式完全就是为了让arr有的接收,不至于收个void报错。

可变参数模板的应用

在Vector中有一个成员函数——emplace_back

它的功能与push_back几乎相同,只不过,如果vector的元素是由多个元素构造的类型,如:pair,

  • push_back只能先构造好,再传

    int main()
    {
    	std::list< std::pair<int, char> > mylist;
    	mylist.push_back(make_pair(40, 'd'));//直接构造pair
    	mylist.push_back({ 50, 'e' });//通过初始化列表构造pair
    	for (auto e : mylist)
    		cout << e.first << ":" << e.second << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • emplace_back则可以通过参数列表的方式传参,再emplace_back内部构造pair,当然构造好再传也可以

    int main()
    {
    	std::list< std::pair<int, char> > mylist;
    	mylist.emplace_back(10, 'a');//参数列表传,内部构造
    	mylist.emplace_back(make_pair(30, 'c'));//构造好再传
    	for (auto e : mylist)
    		cout << e.first << ":" << e.second << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

除了使用上的区别,它们效率也略有区别:

下面我们试一下带有拷贝构造和移动构造的bit::string

int main()
{
	/* 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
	 我们会发现其实差别也不到,emplace_back是直接构造了,
	push_back是先构造,再移动构造,其实也还好。*/
	std::list< std::pair<int, 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

我们会发现

  • emplace_back是直接构造了,
  • push_back是先构造,再移动构造,

其实差距并不大,不过对于一些自定义类型没有移动构造的拷贝的消耗还是比较大的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/1015272
推荐阅读
相关标签
  

闽ICP备14008679号