[The Road to C++ Practice] 30. Variable parameter template && wrapper

insert image description here
Every day without dancing is a disappointment to life

foreword

When learning C language, there have been such functions with variable number of parameters, that is, scanf and printf that we are familiar with, because they can pass any number of parameters: image-20230319122142832and for C++11, C++11 uses This feature is more widely practiced.

The new feature of C++11 variable parameter templates can create function templates and class templates that can accept variable parameters. Compared with C++98/03, class templates and function templates can only contain a fixed number of template parameters. Variable template parameters are undoubtedly a huge improvement. However, due to the abstraction of variable template parameters, certain skills are required to use, so this is still relatively obscure. At this stage, it is enough for us to master some basic variable parameter template features.

1. The first appearance of variable parameter templates

#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

Since the variable parameter list does not support [] access, how can it be accessed?

2. Parameter package expansion

There is an ellipsis in front of the parameter args above, so it is a variable template parameter. We call the parameter with the ellipsis a "parameter pack", which contains 0 to N (N>=0) template parameters. We cannot directly obtain each parameter in the parameter pack args, and can only obtain each parameter in the parameter pack by expanding the parameter pack. This is a main feature of using variable template parameters, and it is also the biggest difficulty, namely How to expand variadic template parameters. Since the grammar does not support the use of args[i] to obtain variable parameters, we use some tricks to obtain the values ​​​​of the parameter pack one by one.

2.1 Recursive function method to expand the parameter pack

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

It can be done by function overloading + recursion, because it can be seen from the template function that the parameters of each recursion will be reduced by 1. When it is reduced to 0, it will not meet the parameter range requirements of the template function. It will call the above function with a parameter of 0, and the recursion will stop at this time.

2.2 Comma expression expansion parameter pack

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

Through comma expressions, the variable parameter list can deduce the size of the array and instantiate the parameters to call. It PrintArg(T t)should be noted that this method cannot pass 0 parameters, which is the above comment ShowList(), because the constant size cannot be allocated. array of 0.

Of course, it can also be optimized:

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

Replace the comma expression with PrintArg with a return value, because the initialization in the array must have a value, except for the comma expression, which is more intuitive than the former.

3. The emplace method of the container

For the emplace and emplace_back methods of various containers, since they are new methods in c++11, whether the parameters are rvalues ​​or lvalues, there is an overloaded function whose variable parameter list is a function, and its function is the same as push and push_back it's the same.

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

Let's demonstrate it:

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

The bottom layer only supports 0 to 1 parameters, then look at the following:

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;
}

Therefore, emplace_back is sometimes faster than push_back, because there is a parameter pack at the bottom layer, and there is no need for copy construction. Of course, emplace_back can also pass objects directly.

image-20230319141427720


This can be seen why emplace_back() is faster. If the move construction is not implemented, the difference between the two will be very large. (Especially for some classes with more content: such as string, etc.)

image-20230319150546794

emlplace is one less copy, direct construction, and no copy process on parameters. Therefore, for classes that do not implement deep copy of mobile construction, what is reduced is one deep copy, and the performance will be greatly improved.

The first three headings are all introductions to variadic templates, followed by a new topic: Wrappers.

4. Wrapper

C language function pointers, C++ functors/functor objects, and lambdas are all learned before. Today, a new wrapper is added: function

4.1 What is a function

Function wrappers are also called adapters. The function in C++ is essentially a class template and a wrapper. (actually a class template)

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

4.2 The role of the function wrapper

For code like this:

#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;
}

In the above method, the reason for the inefficiency is that the template is instantiated three times by different classes, how to prove it?image-20230328163710973

As count is a static type, the address is different each time, so it can be seen that it is instantiated three times. In order to prevent the inefficiency caused by this method, make it instantiate only one copy, and let this place be unified, which uses the function:

The role of the function wrapper: to unify the types of various callable objects

#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

The bottom f6 is not of the same type as the one above, but it can be made into the same type as the previous one through special processing, namely:

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()

Therefore, we can also use a wrapper to set the code that is instantiated three times at the beginning to the same type:

#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

Through such wrapper processing, there will be no inefficiency caused by multiple instantiations due to different types.

4.3 The practical use of function

Response between symbols and functions is possible

For example, Leetcode150. Inverse Polish expression evaluation can be written in 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

Through the above method, even if a new operation is added, it is only necessary to continue adding it in opFuncMap.

4.4 What is bind

The std::bind function is defined in the header file and is a function template. It is like a function wrapper (adapter), accepting a callable object (callable object), and generating a new callable object to "adapt" to the original object list of parameters. Generally speaking, we can use it to take a function fn that originally received N parameters, and return a new function that receives M parameters (M can be greater than N, but it doesn't make sense to do so) by binding some parameters. At the same time, using the std::bind function can also implement operations such as parameter order adjustment.

// 原型如下:
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 The role of bind

bind binding can reduce the number of parameters passed when using.

#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

Guess you like

Origin blog.csdn.net/NEFUT/article/details/130780158