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

可变参数模板的概念

可变参数模板的定义方式

参数包的展开

递归式展开参数包

逗号表达式展开参数包

emplace

emplace 的使用

emplace 的优势

可变参数模板的概念

在C++11之前,函数模板和类模板中的模板参数数量是固定的。可变参数模板打破了这个限制,提供了一种编写泛型代码的方法,让我们可以定义接受可变数量参数的模板。这极大地增加了模板的灵活性和表达能力。


可变参数模板的定义方式

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


//Args是一个模板参数包

//args是一个函数参数包

//声明一个参数包Args... args,这个参数包中可以包含0到任意数量个模板参数。

template <class ... Args>

void ShowList(Args... args)

{


}

1

2

3

4

5

6

7

8

模板参数包:template<class ... Args> 中的Args... 表示一个模板的参数包,它可以包含0个或多个模板参数。

函数参数包:void ShowList(Args... args)中的args... 表示一个函数的参数包,允许函数接受任意数量的参数。

...在参数包中的作用


... 在模板参数包中的作用: 用于声明一个可以包含多个参数的包,一般位于参数名称之前。

... 在函数参数包中的作用: 用于表示这个函数可以接受多个参数,一般位于参数名称之后。

当定义了一个可变参数模板之后,就可以像普通函数一样调用它:


//Args是一个模板参数包

//args是一个函数参数包

//声明一个参数包Args... args,这个参数包中可以包含0到任意数量个模板参数。

template <class ... Args>

void ShowList(Args... args)

{


}


int main()

{

ShowList(1, 2, 3); // 可以传入任意数量的参数

ShowList();       // 也可以传入0个参数

ShowList(1, 2, 3, "string");//也可以传入不同类型的参数

return 0;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

也可以通过sizeof来打印参数的个数:



为什么这里的调用时sizeof...(args)?


这里的sizeof...是一个操作符,用于计算参数包中元素的数量,其必须与参数包一起使用,而args是一个函数参数包的名称,表示在函数调用的时候传入的参数。

sizeof...是一个变体,专门用以计算参数包的大小,它只能与参数包一起使用。这个是C++11的规定。


原理:


当你调用ShowList(1,2,3)的时候,args被推导为{1,2,3},这是一个包含三个参数的包。

sizeof...(args)计算的是args中的元素数量,结果为3。

在编译期间,编译器会根据传入的参数数量和类型来确定args的具体内容,并计算出数量。

但是我们仍不能方便的随机获取参数,同时可变参数的语法也不支持args[i],所以如何获取就值得我们探讨一番!


为什么可变参数模板的语法不支持args[i]呢?


可变参数模板是编译时编程

可变参数模板是在编译期间展开的,编译器需要根据参数包的内容生成对应的代码。在编译期间,参数包中的参数是未知的,因此无法确定具体的下标。

参数包是一个整体

在可变参数模板中,参数包args...是一个整体传递的,它代表了0个或者多个参数,无法像数组那样通过下标访问单个参数。

参数包需要展开

要访问参数包中的单个参数,需要对参数包进行展开(后续会谈论)。常见的展开方式由递归展开、逗号表达式、初始化列表展开等。这些展开方式都是基于对参数包的遍历,而不是通过下标访问。

参数包的长度是编译常量

通过sizeof...(args)可以在编译期间获取参数包长度。但这个长度是编译期常量,无法在运行时改变。因此,参数包的长度是固定的,无法像数组一样动态改变。

参数包的展开

递归式展开参数包

 递归展开参数包实际上是通过逐步剥离参数包中的元素来实现的。具体来说,对于下面的代码,编译器在编译的时候会根据传入的实参推导出模板参数的类型,并且生成相应的函数调用。每次递归调用都会减少参数包的大小,直到仅剩一个为止。


template<class T>

void ShowList(T value)

{

cout << value << endl;

}


template<class T,class ...Args>

void ShowList(T value, Args... args)

{

cout << value << " ";

ShowList(args...);

}


int main()

{

ShowList(1, 2, 3);

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17


调用情况示例

当我们调用 ShowList(1, 2, 3) 时,调用栈的变化过程如下:


// 第一次调用

ShowList<int, int, int>(1, 2, 3);

   // 打印 1

   ShowList<int, int>(2, 3);

       // 打印 2

       ShowList<int>(3);

           // 打印 3

           // 递归终止

1

2

3

4

5

6

7

8

其次也可以这样写,写一个不带参的递归终止函数,当传入的args为空的时候,就会直接匹配第一个无模板无参的函数。



逗号表达式展开参数包

逗号表达式可以用来展开参数包,它的基本思路如下:


将逗号表达式的最后一个表达式设置为一个整型值,确保逗号表达式返回的是一个整型值。

将处理参数包中参数的动作封装成一个函数,将该函数的调用作为逗号表达式的第一个表达式。

在列表初始化时使用逗号表达式展开参数包。

这样一来,在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。


下面是一个代码示例:




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, 2, 3);

cout << "---------------" << endl;

ShowList("sad", 1);

return 0;

}


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23



通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。


那么为什么要返回一个 0 呢?


这是因为在列表初始化数组时,每个元素的表达式都需要返回一个值。如果不返回任何值,编译器会报错。所以,我们选择返回一个 0 作为占位符,因为这个值最终不会被使用到。关键是,逗号表达式会先执行左边的表达式,也就是打印参数,然后返回右边的值。

展开后,{ (PrintArg(arg1), 0), (PrintArg(arg2), 0), ... } 这个列表初始化会被展开成多个逗号表达式,每个表达式都会打印出对应的参数,并返回一个 0。这些 0 值会被用于初始化数组 arr。

总之, 在使用逗号表达式展开参数包时,返回一个占位值是为了满足列表初始化的语法要求,而真正的目的是在逗号表达式的左边执行一些操作,比如打印参数。


emplace

emplace 的使用

emplace 是 C++11 中引入的一组成员函数,主要用于 STL 容器(如 vector、list、deque 等),允许在容器中直接构造元素,而不是先创建对象再将其拷贝到容器中。


基本用法

emplace 系列函数包括 emplace_back、emplace_front 和 emplace,它们的基本用法如下:

emplace_back: 在容器的末尾构造一个新元素。

emplace_front: 在容器的开头构造一个新元素。

emplace: 在指定位置之前构造一个新元素。

示例代码:


#include <iostream>

#include <vector>

#include <string>


class Person {

public:

   Person(int age, const std::string& name) : _age(age), _name(name) {

       std::cout << "Constructing: " << name << std::endl;

   }

   int _age;

   std::string _name;

};


int main() {

   std::vector<Person> people;


   // 使用 emplace_back 直接构造对象

   people.emplace_back(25, "Alice");

   people.emplace_back(30, "Bob");


   // 使用 emplace 在指定位置构造对象

   people.emplace(people.begin() + 1, 22, "Charlie"); // 在 Bob 前面插入 Charlie


   for (const auto& person : people) {

       std::cout << person.name << " is " << person.age << " years old." << std::endl;

   }


   return 0;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29



emplace 的优势

效率更高的原因


直接构造:

使用 emplace 系列函数时,传递的参数会直接用于构造对象,而不是先创建一个临时对象再将其拷贝到容器中。这减少了不必要的拷贝或移动构造。

避免临时对象:

当使用 push_back 或 insert 时,通常需要创建一个临时对象,然后将其拷贝到容器中。这会导致额外的构造和析构开销。而 emplace 直接在容器内部构造对象,避免了这个过程。

支持参数包:

emplace 支持可变参数模板,可以接受任意数量和类型的参数,这使得它在构造复杂对象时更加灵活。例如,可以直接传递多个参数来构造一个对象,而不需要先创建一个临时对象。

何时不优于传统插入


如果传入的是左值对象或右值对象,emplace 的效率可能与传统的 push_back 或 insert 相当,因为在这些情况下,仍然可能会调用拷贝构造函数或移动构造函数。

emplace 的优势主要体现在构造新对象时,尤其是当传入参数包时,可以避免创建临时对象和拷贝构造。