【C++11】—— 可变参数模板

目录

一、可变参数模板概念以及定义方式

二、参数包的展开

1. 递归函数方式展开参数包

2. 逗号表达式展开参数包

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


一、可变参数模板概念以及定义方式

        在c++11之前,类模板和函数模板只能含有固定数量的模板参数,c++11增加了可变模板参数特性:允许模板定义中包含0到任意个模板参数。声明可变参数模板时,需要在typename或class后面加上省略号"..."。 

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
// 模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args
template <class ...Args>
void ShowList(Args... args)
{}

现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。比如:

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

我们可以在函数模板中通过sizeof计算参数包中参数的个数。比如:

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

二、参数包的展开

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

1. 递归函数方式展开参数包

递归展开参数包的方式如下:

  • 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
  • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
  • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来来。
  • 还需要给一个递归终止函数。
// 递归终止函数
// 当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。
template <class T>
void ShowList(const T& t)
{
    cout << t << endl;
}

// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
    cout << value <<" ";
    ShowList(args...);
}

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

2. 逗号表达式展开参数包

我们知道逗号表达式会按顺序执行逗号前面的表达式。

        (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数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template <class T>
void PrintArg(T t)
{
    cout << t << " ";
}

//展开函数
template <class ...Args>
void ShowList(Args... args)
{
    int arr[] = { (PrintArg(args), 0)... };
    cout << endl;
}

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

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

        C++11标准给STL中的容器增加emplace版本的插入接口,比如vector容器的push_back和insert函数,都增加了对应的emplace_back和emplace函数。如下:

我们来看一下他们的声明

push_back在C++11之后除了原因的左值版本外,还提供了右值版本,如果push_back的是左值那么就调用左值版本,反之就是右值引用的版本;但是只能支持单个元素的插入。

emplace_back和emplace本质的区别就是他们采用了可变参数模板,这样一来,他就可以支持多个元素的插入,代码如下:

int main()
{
	/********************emplace_back***************************/
	vector<int> v1;
	vector<int> v2;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	//v1.push_back({1,2,3,4,5}); // 不可以

	v2.emplace_back(1, 2, 3, 4, 5);

	/**********************emplace*************************/
	v1.insert(v1.begin(), 9);
	v1.insert(v1.begin(), 8);
	v1.insert(v1.begin(), 7);
	v1.insert(v1.begin(), 6);
	//v1.insert(v1.begin(), 9, 8, 7, 6); // 不可以

	v2.emplace(v2.begin(), 9, 8, 7, 6);

	return 0;
}

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

  •  如果调用emplace系列接口时传入的是左值对象,那么首先需要先在此之前调用构造函数实例化出一个左值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数。
  • 如果调用emplace系列接口时传入的是右值对象,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数。
  • 如果调用emplace系列接口时传入的是参数包,那就可以直接调用函数进行插入,并且最终在使用定位new表达式调用构造函数对空间进行初始化时,匹配到的是构造函数。 

总结一下:

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

猜你喜欢

转载自blog.csdn.net/sjsjnsjnn/article/details/128687016