【C++修炼之路】30.可变参数模板&&包装器

在这里插入图片描述
每一个不曾起舞的日子都是对生命的辜负

前言

在学习C语言时,就有过这种可变的参数数量的函数,即我们耳熟能详的scanf和printf,因为其可以传任意数量的参数:image-20230319122142832而对于C++11来说,C++11使这个特性实践的更加广泛。

C++11的新特性可变参数模板能够创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段,我们掌握一些基础的可变参数模板特性就够我们用了。

一.可变参数模板的首次登场

#include<iostream>
#include<vector>
using namespace std;

//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包可以包含0到任意个模板参数。
template<class ...Args>
void ShowList(Args... args)
{
    
    
	//查看参数包中有几个参数
	cout << sizeof...(args) << endl;
	//for (size_t i = 0; i < sizeof...(args); i++)//可惜的是可变参数列表不支持[]重载
	//{
    
    
	//	cout << args[i] << endl;
	//}
}

int main()
{
    
    
	//想传几个就传几个,想传什么类型就传什么类型
	ShowList();
	ShowList(1);
	ShowList(1, 1.1);
	ShowList(1, 1.1, string("xxxxxx"));
	return 0;
}

image-20230319123459880

既然可变参数列表不支持[]访问,那么如何能够进行访问呢?

二.参数包展开

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

2.1 递归函数方式展开参数包

void ShowList()
{
    
    
	cout << endl;
}

//args参数包可以接收0-N个参数包,而下面这个由于存在T就接收1-N个参数包
template<class T, class ...Args>
void ShowList(T val, Args... args)
{
    
    
	cout << val << " ";
	ShowList(args...);
}

int main()
{
    
    
	ShowList();
	ShowList(1);
	ShowList(1, 1.1);
	ShowList(1, 1.1, string("xxxxxx"));
	return 0;
}

image-20230319125830142

通过函数重载+递归的方式就可以完成,因为从模板函数中可以看出每次递归的参数都会减少1个,当减少到0个的时候,就会因为不满足模板函数的参数范围要求,就会去调用上面参数为0的函数,此时就停止递归了。

2.2 逗号表达式展开参数包

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();
	ShowList(1);
	ShowList(1, 1.1);
	ShowList(1, 1.1, string("xxxxxx"));
	return 0;
}

image-20230319131508405

通过逗号表达式的方式,可变参数列表能够推演数组的大小并将参数进行实例化从而调用PrintArg(T t),需要注意的是,这种方式不能传0个参数,即上面注释的ShowList(),因为不能分配常量大小为 0 的数组。

当然,也可以优化一下:

template<class T>
int PrintArg(T t)
{
    
    
	cout << t << " ";
	return 0;
}

//展开函数
template<class ...Args>
void ShowList(Args... args)
{
    
    
	//逗号表达式:结果为后面的值,通过可变参数列表展开并推演个数,进行实例化调用上面的函数。
	int arr[] = {
    
     PrintArg(args)... };
	cout << endl;
}

int main()
{
    
    
	//ShowList();
	ShowList(1);
	ShowList(1, 1.1);
	ShowList(1, 1.1, string("xxxxxx"));
	return 0;
}

image-20230319132952697

将逗号表达式换成PrintArg带有返回值的方式,因为数组里面初始化必须有值,除了逗号表达式就是这种方式,相比前者,这种更直观。

三.容器的emplace方法

对于各种容器的emplace、emplace_back方法,由于是c++11新出的方法,参数无论是右值还是左值,都存在一个可变参数列表为函数的重载函数,其功能与push、push_back是一样的。

template <class... Args>
void emplace_back (Args&&... args);

下面就来演示一下:

int main()
{
    
    
	list<int> list1;
	list1.push_back(1);
	list1.emplace_back(2);
	list1.emplace_back();

	//下面的参数过多底层就不识别了
	//list1.emplace_back(3, 4);
	//list1.emplace_back(3, 4, 5);

	for (auto& e : list1)
	{
    
    
		cout << e << endl;
	}

	return 0;
}

image-20230319135926739

底层仅仅支持0到1个参数,接下来看看下面:

int main()
{
    
    
	list<pair<int, char>> mylist;
	mylist.push_back(make_pair(1, 'a'));//构造+拷贝构造
	//mylist.push_back(1, 'a'); //push_back不支持
    
	//emplace_back支持
	mylist.emplace_back(1, 'a');//直接构造
	for (auto& e : mylist)
	{
    
    
		cout << e.first << " : " << e.second << endl;
	}

	return 0;
}

所以,emplace_back有时比push_back更快,就是因为其底层存在着参数包,不用进行拷贝构造。当然,emplace_back也可以直接传对象。

image-20230319141427720


这就可以看出,为什么通过emplace_back()更快,如果没有实现移动构造,那么这两个的差别就会非常的大。(尤其是对于一些内容较多的类:如string等)

image-20230319150546794

emlplace就是少拷贝一次,直接构造,没有参数上的拷贝过程,因此如果对于没有实现移动构造的深拷贝的类,减少的就是一次深拷贝,性能就会提升很多。

前三个标题都是介绍的可变参数模板,下面是新的主题:包装器。

四.包装器

c语言的函数指针,C++的仿函数/仿函数对象、lambda都是之前学过的,今天新增一个包装器:function

4.1 什么是function

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。(实际上是类模板)

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;   // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

4.2 function包装器的作用

对于如下代码:

#include<iostream>
using namespace std;

ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
template<class F, class T>
T useF(F f, T x)
{
    
    
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
    
    
	return i / 2;
}
struct Functor
{
    
    
	double operator()(double d)
	{
    
    
		return d / 3;
	}
};
int main()
{
    
    
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lambda表达式
	cout << useF([](double d)->double {
    
     return d / 4; }, 11.11) << endl;
	return 0;
}

上述方式,导致效率低下的原因是该模板被不同的类实例化了三次,如何证明?image-20230328163710973

count作为static类型,每一次的地址都不同,所以可以看出,实例化了三次。为了防止这种方式造成的效率低下,使其只实例化一份,让这个地方统一一下,这就利用到了function:

function包装器的作用: 对各种可调用对象进行类型统一

#include<functional>
using namespace std;

int f(int a, int b)
{
    
    
	return a + b;
}

struct Functor
{
    
    
public:
	int operator() (int a, int b)
	{
    
    
		return a + b;
	}
};

class Plus
{
    
    
public:
	static int plusi(int a, int b)
	{
    
    
		return a + b;
	}

	int plusd(int a, int b)
	{
    
    
		return a + b;
	}
};
int main()
{
    
    
	//不是特化,而是和vector一样,是类的实例化,但传统的都是一个一个参数,function不一样
	//1.封装函数指针:两种初始化方式
	function<int(int, int)> f1;
	f1 = f;
	function<int(int, int)> f2(f);


	//2.封装仿函数对象
	f1 = Functor();

	//function<int(int, int)> f3(Functor()); 有点怪,vs和g++(版本4.8)都识别不了,可能是把这个看成函数指针了

	//Functor ft;
	//function<int(int, int)> f3(ft);//这种就可以
	//函数对象
	function<int(int, int)> f3 = Functor();
	cout << f1(1, 2) << endl;
	cout << f3(1, 2) << endl;


	//3.封装lambda
	function<int(int, int)> f4 = [](const int a, const int b) {
    
     return a + b; };
	cout << f4(1, 3) << endl;

	//4.封装成员函数: 函数指针
	function<int(int, int)> f5 = &Plus::plusi; //类静态成员函数指针--static修饰的&可加可不加
	cout << f5(1, 2) << endl;
	// -----------------------------------------------------------------------------------以上都是同一个类型
	function<int(Plus, int, int)> f6 = &Plus::plusd;//需要加上类名
	cout << f6(Plus(), 1, 2) << endl;//因为this指针不能显式调用,所以需要直接加Plus()

	return 0;
}

image-20230328172103074

对于最下面的f6与上面的不是同一个类型,但是可以通过特殊处理让其与之前的变成一个类型,即:

Plus plus;
function<int(int, int)> f6 = [&plus](int x, int y)->int {
    
    return plus.plusd(x, y); };
cout << f6(1, 2) << endl;//因为this指针不能显式调用,所以需要直接加Plus()

因此,最开始实例化三次的代码我们也可以用包装器让其置为同一类型:

#include<functional>
int main()
{
    
    
	// 函数名
	cout << useF(function<int(double)>(f), 11.11) << endl;
	// 函数对象
	Functor ft;
	cout << useF(function<int(double)>(ft), 11.11) << endl;
	// lamber表达式
	cout << useF(function<int(double)>([](double d)->double {
    
     return d / 4; }), 11.11) << endl;
	return 0;
}

image-20230328194822734

通过这样的包装器处理就不会因为类型不同导致实例化多次造成效率低下了。

4.3 function的实际用途

可以进行符号和函数之间的响应

就比如Leetcode150.逆波兰表达式求值,就可以通过c++11的方式进行编写:

class Solution {
    
    
public:
    int evalRPN(vector<string>& tokens) {
    
    
        stack<int> st;
        map<string, function<int(int, int)>> opFuncMap = 
        {
    
    
            {
    
    "+", [](int x, int y)->int{
    
    return x + y;}},
            {
    
    "-", [](int x, int y)->int{
    
    return x - y;}},
            {
    
    "*", [](int x, int y)->int{
    
    return x * y;}},
            {
    
    "/", [](int x, int y)->int{
    
    return x / y;}}
        };

        for(auto& str : tokens)
        {
    
    
            if(opFuncMap.count(str) == 0)
            {
    
    
                st.push(stoi(str));
            }
            else
            {
    
    
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();

                st.push(opFuncMap[str](left, right));
            }
        }

        return st.top();
    }
};

image-20230328190813051

通过以上的方式,即便又增加了一种新的操作,只需要在opFuncMap里面继续添加就好了。

4.4 什么是bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

4.5 bind的作用

bind绑定可以减少使用时参数传递的个数。

#include<iostream>
#include<functional>
using namespace std;
int Plus(int a, int b)
{
    
    
	return a + b;
}

int SubFunc(int a, int b)
{
    
    
	return a - b;
}
class Sub
{
    
    
public:
	int sub(int a, int b)
	{
    
    
		return a - b * x;
	}
private:
	int x = 20;
};

int main()
{
    
    
	//表示绑定函数plus,参数分别由调用 func1 的第一、二个参数指定,占位对象
	function<int(int, int)> func1 = bind(Plus, placeholders::_1, placeholders::_2);
	cout << func1(1, 2) << endl;
	function<int(int, int)> func2 = bind(SubFunc, placeholders::_1, placeholders::_2);
	cout << func2(1, 2) << endl; // -1

	//调整参数的顺序
	function<int(int, int)> func3 = bind(SubFunc, placeholders::_2, placeholders::_1);
	cout << func3(1, 2) << endl; // 1

	function<bool(int, int)> gt = std::bind(less<int>(), placeholders::_2, placeholders::_1);
	cout << gt(1, 2) << endl;

	//固定绑定参数:减少参数的传递
	function <int(Sub, int, int)> func4 = &Sub::sub;
	cout << func4(Sub(), 10, 20) << endl;//-390
	//绑定之后相当于减少了参数的个数,_1和_2代表参数传递的顺序
	function <int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
	cout << func5(10, 20) << endl;//-390

	return 0;
}

image-20230329152449443

猜你喜欢

转载自blog.csdn.net/NEFUT/article/details/130780158
今日推荐