当前位置:   article > 正文

C++可变参数模板(详解)_c++ 可变参数模板

c++ 可变参数模板

可变参数模板

引入

给你一个问题?

编写一个简单的 print 函数,实现接受任意有限数量的参数,并输出。类似python的print
函数。

例如:
bool b = true;
print(12, ‘c’, 3.14, false, b, “hello”);
// output
// 12 c 3.14 false true hello

基础讲解

可变参数 :参数的数目是可变的,不固定的。即数目可以是任意有限个。

模板 :参数的类型是一种模板,是可经推导的,可以是任意存在的类型(系统类型或自定义类型)。

一个可变参数模板就是一个可以接受可变参数的模板函数或模板类。

形式如下:

template <typename... Args>
void func(Args... args) {}

template <class... Args>
class Obj {};
  • 1
  • 2
  • 3
  • 4
  • 5

参数包:可变数目的参数。

分析上面代码

template <typename... Args> ,定义了一个参数包,这包存放的可变参数的数据类型。类似于 template <typename T>,这是这里用 typename...代替了typename,用来说明定义的一个参数包(可以有多个类型数据)。Args 参数包名称,可更改,但我们常命名为 Args , 习惯所致。

void func(Args... args) {}, 用上面定义的数据类型参数包Args,定义一个相对应的存放数据的数据包。Args... args 可以理解为对类型包 Args 依次取出数据类型,用来定义数据,最后把数据放入数据包 args 中。

注意:Args,args有的书上分别定义为模板参数包和函数参数包。 上面我说的Args是数据类型包,args是数据包,只是为了更好的理解Args和args。其实,我认为叫什么不重要,主要是你能更好的理解它。

int i = 2; 
char c = 'A';
double d = 3.14;

func(i, c, d);  // 包中有三个参数
func(i, c);		// 包中有两个参数
func(i);		// 有一个参数
func();			// 空包
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

编译器会为 func 分别实例化出四个不同的版本

void func(int, char, double); 
void func(int, char); 
void func(int); 
void func(); 
  • 1
  • 2
  • 3
  • 4

sizeof… 运算符

到现在,我们已经学会简单的定义一个可变参数包。我们如何使用这个参数包的。

借助sizeof...运算符,获得参数包中元素的个数。

template<typename... Args>
void putSize(Args... args)
{
    cout << "sizeof...(Args) : " << sizeof...(Args) << endl; // 类型参数的数目
    cout << "sizeof...(Args) : " << sizeof...(Args) << endl; // 数据参数的数目
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

更进一步,我们可以写个工具类,包装一下 sizeof…

#include <iostream>
using namespace std;

// 工具类,返回参数包中元素数目
template<class... Args>
struct Count
{
    static const std::size_t value() { return sizeof...(Args); }
};

template<class... Args> 
void f(Args... args)   
{
    cout << sizeof...(Args) << endl;  // 5
    cout << sizeof...(args) << endl;  // 5
    
    cout << Count<Args...>::value() << endl; 	 		// 5
    cout << Count<decltype(args)...>::value() << endl; 	// 5
}

int main()
{
    f(1, 2, 'c', 3.14, "hello");
    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

包展开(解包)

  1. C++不允许对通过[] 的方式进行解包, args[0] 是错误的。

  2. C++的包展开是通过 args... 的形式,后面... 就意味着展开包。

template <typename T, typename... Args> void func(T v, Args... args) 
{
    func(args...); // 形如 func(arg1, arg2, arg3);
}

int i = 2;  double d = 1.8; char c = 'c';
func(i, d, c); // Args 中包含三个类型数据 int、double、char  // args 包含三个数据 2、1.8、'c'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上面是直接展开包,存进去什么,取出来什么。

但是如果现在有个需求,已知args 中存放的是 int i, double d, char c, 但我要求是展开后取出的数据都是数据对应的地址,即 &i, &d, &c,又该如何解包。

C++提供一种机制,可以实现。&args... 就可实现,每次取出的数据 arg 自动转换成 &arg。

验证一下。

#include <iostream>

using namespace std;

void p1(int a, int b)
{
    cout << "a: " << a << " b: " << b << endl;
}

void p2(int *a, int *b) // 接受指针参数
{
    cout << "a: " << *a << " b: " << *b << endl;
}

template <class... Args>
void f1(Args... args)
{
    p1(args...);    // ...代表解包 展开为 p1(arg1, arg2);
    
    p2(&args...);   // &代表解包模式 &args... 被解包为 &arg1, &arg2, &arg3等等
    			  // 展开为 p2(&arg1, arg2);
}

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

/*output
	a: 2 b: 3
	a: 2 b: 3
*/
  • 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
  • 30
  • 31
  • 32
  • 33
  • 34

更进一步,我们可以这样解包。

f(&args...);             // 展开成 f(&E1, &E2, &E3)
f(n, ++args...);         // 展开成 f(n, ++E1, ++E2, ++E3);
f(++args..., n);         // 展开成 f(++E1, ++E2, ++E3, n);

f(const_cast<const Args*>(&args)...); // f(const_cast<const E1*>(&X1), const_cast<const E2*>(&X2), const_cast<const E3*>(&X3))

f(h(args...) + args...); // 展开成 f(h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)

int res[size] = {1, args..., 2}; // 展开成 {1, arg1, arg2, arg3, 2}
Class c2 = Class(n, ++args...);  // 调用 Class::Class(n, ++E1, ++E2, ++E3);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

递归解包

利用C++的模板推导来实现。

要求:

  1. 需要一个递归解包函数,用来递归解包。
  2. 需要一个终止递归函数,用来终止递归。
void f() { cout << endl; }  // 如果 args 为空包, 即包已经全部展开,此时匹配此函数,结束递归。

template<typename T, typename... Args>
void f(T v, Args... args)
{
    cout << v << ' ';
   	f(args...);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

按照模板的推导规则,f 函数的调用规则是这样的。

依次从 args 参数包中取出一个参数放到 v 中, 剩下的依然放在包中。

void f<int, double, char>(int v, double __args1, char __args2);
void f<double, char>(double v, char __args1);
void f<char>(char v);
void f();
  • 1
  • 2
  • 3
  • 4

Lambda 捕获

// Lambda 捕获
template<typename... Args>
void f2(Args... args)
{
    auto lm = [&, args...] { return p1(args...); };
    lm();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

转发参数包

我们依然可以借助forward机制来编写函数。实现将参数原封不动的传递给其他函数。

注意:这是 std::forward 完美转发的知识。还包括左右值引用,万能引用,引用折叠等细节知识。不是我们文章的重点,我们就不展开说了。

一般右值引用的传递,我们使用std::forward<type>(value) 就行。对于参数包略有不同。借助std::forward<Args>(args)... 就可以实现参数的完美转发了。

template<typename T, typename... Args>
void f(T v, Args... args)
{
   	f(std::forward<type>(value)...);
}
  • 1
  • 2
  • 3
  • 4
  • 5

实战

要求: 编写一个简单的 print 函数,实现接受任意有限数量的参数,并输出。

#include <iostream>

// 终止递归函数
void print(){
    std::cout << std::endl;
}

// 递归解包函数
template <class T, class... Args>
void print(T&& val, Args&&... args)
{
    
    std::cout << std::boolalpha << val << " ";
    print(std::forward<Args>(args)...);  // 转发参数包
}

int main()
{
    bool b = true;
    print(12, 'c', 3.14,  false, b, "hello");
    return 0;
}

/*output
	12 c 3.14 false true hello 
*/
  • 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
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/577003
推荐阅读
相关标签
  

闽ICP备14008679号