当前位置:   article > 正文

【C++11】可变参数模版/lambda表达式/包装器_可变参数模板展开

可变参数模板展开

一:可变参数模版

           1.1:什么是可变参数模板

           1.2:可变参数包的两种展开方式(递归/逗号表达式)

           1.3:可变参数模版的应用

二:lambda表达式

           2.1:见一见lambda表达式

           2.2:lambda表达式的语法规则

           2.3:lambda表达式的捕捉列表

          2.4:lambda表达式的底层原理

三:包装器

         3.1:function包装器

         3.2:bind(绑定)

一:可变参数模版

 1.1:什么是可变参数模板

在C++11中,可变参数模板(variadic templates)是一种特殊的模板类型,可变参数模板能够让您创建可以接受可变参数的函数模板和类模板!

相比C++11,C++98/03中类模版和函数模版中只能含固定数量的模版参数,C++11中的可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以我们只需了解并知道其展开方式即可!!!

虽然可变参数模板是C++11后的新特性,但是在C++11之前,我们早已接触过可变参数,我们以前经常使用的printf函数(int printf(const char*format,...))就可以接受任意多个参数(可变参数:...),但是这也只是函数的可变参数,并不是模板的可变参数。那么模版的可变参数是什么样的呢???

  1. // Args是一个模板参数包,args是一个函数形参参数包
  2. // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
  3. template <class ...Args>
  4. void func(Args... args)
  5. {}

上面就是模版的可变参数,其中,参数args前面有省略号(省略号是关键!),所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。

同时模板参数包与函数形参参数包名称是任意的,想写成什么就是什么,Args与args并不是关键字,只是我们一般写成Args与args,方便理解!

下面我们展示可变参数模版的使用:

  1. // Args是一个模板参数包,args是一个函数形参参数包
  2. // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
  3. template<class ...Args>
  4. void func(Args ...args)
  5. {
  6. //我们可以通过sizeof来打印参数包中参数的个数!
  7. cout << sizeof...(args) << endl;/要只有...的位置,容易搞混!!!
  8. }
  9. int main()
  10. {
  11. //我们可以传任意多个参数,且这些参数可以是不同类型的!!!
  12. func(1);
  13. func(2, 'a');
  14. func(3, 'a', "B");
  15. func(4, 'a', "B", string("string"));
  16. return 0;
  17. }

我们无法直接获取参数包args中的每个参数的,最多通过sizeof来计算参数包中参数的总个数,所以我们只能通过展开参数包的方式来获取参数包中的每个参数,但这也是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

既然我们能够通过sizeof获得参数包的个数,那么我们是否可以通过for循环以及args[i]来展开参数包呢???

此时我们看到,由于语法不支持使用args[i]这样方式获取可变参数,所以这样无法展开可变参数包!所以我们可以用一些奇招来一一获取参数包的值!!!

1.2:可变参数包的两种展开方式(递归/逗号表达式)

1.2.1:递归方式展开可变参数包

上面就是递归展开参数包的基本代码,其大致展开思想是:
1:再定义一个模版参数,用于接受参数包中分离出的第一个参数,

2:然后递归调用自己,将剩下的参数包接着解析!

3:当参数包解析完了,那么此时递归调用的就是无参终止函数,此时我们就结束了递归!

具体过程如下所示:

但是这里存在一个问题,

就是当我们直接无参调用带有可变参数模版的函数(func(const T&val,Args...args))时,那么它将直接匹配到递归终止函数(func()),而我们的本意是当外部调用带有可变参数模版的函数时,无论它有多少个参数,哪怕是0个参数,都要通过带有可变参数模版的函数来处理,所以此时,我们可以再嵌套一层,让第一层调用时调用的函数只有可变参数模板!具体如下所示:

  1. //递归展开终止函数:
  2. void func_agrs()
  3. {
  4. cout << endl;
  5. }
  6. //递归展开函数:
  7. template<class T, class ...Args>
  8. void func_agrs(const T& val, Args ...args)
  9. {
  10. cout << val << "--->";//打印参数包中的参数!
  11. cout << " 参数包中的参数个数:"<<__FUNCTION__ << "(" << sizeof...(args) << ")" <<
  12. endl;//打印每一次递归解析后的参数包中参数的个数
  13. func_args(args...);//然后将剩下的参数包接着解析!
  14. }
  15. //给外部提供的只有可变参数模版的展开函数!
  16. template<class ...Args>
  17. void func(Args ...args)
  18. {
  19. func_agrs(args...);
  20. }
  21. int main()
  22. {
  23. func();//此时我们无参调用时,它依旧匹配的是只有可变参数模板的展开函数,只不过它递归时是直接调用递归终止函数而已!
  24. func(4, 'a', "B", string("string"));//这个还是按照正常递归展开可变参数包!
  25. return 0;
  26. }

这样无论外部调用时,无论是多少个参数,哪怕是0个参数,都调用的是带有可变参数模板的函数!

1.2.2:逗号表达式展开可变参数包

上面就是逗号表达式展开参数包的基本代码,其大致展开思想是:
逗号表达式展开参数包的方式,不需要通过递归终止函数, printargs函数不是一个递归终止函数,只是一个处理参数包中每一个参数的函数

这种就地展开参数包的方式实现的关键是逗号表达式。

我们知道逗号表达式会按先后顺序执行逗号前面的表达式。
func函数中的逗号表达式:(printargs(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0,。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, 所以此时(printargs(args), 0)这个逗号表达式就分成了两部分:

第一部分:通过逗号表达式的特性先执行printargs(args),调用printfargs函数处理参数包中每一个参数!

第二部分:根据逗号表达式的结果通过初始化列表来初始化array数组!!!

综上: {(printargs(args), 0)...}将会展开成{(printargs(arg1),0),(printargs(arg2),0), (printargs(arg3),0)......(printargs(argn),0)},

最终会创建一个元素值都为0的数组int array[sizeof...(Args)]

由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printargs(args)函数从而
打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在
数组构造的过程展开参数包!!!

同时,和递归展开一样,当我们无参调用时,会出现问题,逗号表达式展开参数包因为是在初始化array时展开,那么当无参调用时,可能会引发array数组的大小为0,而数组大小不能为0,所以我们需要像递归展开那样,提供一个支持无参调用的函数,具体如下所示:

当然,我们也可以不使用逗号表达式,因为初始化整形数组是必须是整形,所以我们也可以直接通过printfargs函数的返回值做到在初始化array数组的同时,展开参数包,具体如下所示:

 1.3:可变参数模版的应用

C++11之后,STL库中,容器基本上都增加了一个emplace系列的插入接口!我们以list为例:

这些emplace系列的接口都支持可变参数模板的使用,对标之前的push_front,push_back,insert这些插入接口函数,其声明如下所示:


注意,这些emplace系列的插入接口所使用的可变参数模版都加上“&&”,这个并不是表示右值引用,而是表示万能引用那么emplace系列的接口与之前的哪些插入接口有什么区别吗?为了我们方便打印调试,我们先写一个简单版本的自己实现的string类,用于观察,具体如下所示:

  1. namespace MKL
  2. {
  3. class string
  4. {
  5. public:
  6. typedef char* iterator;
  7. iterator begin()
  8. {
  9. return _str;
  10. }
  11. iterator end()
  12. {
  13. return _str + _size;
  14. }
  15. string(const char* str = "")
  16. :_size(strlen(str))
  17. , _capacity(_size)
  18. {
  19. _str = new char[_capacity + 1];
  20. strcpy(_str, str);
  21. cout << "string(const char* str = "")" << endl;
  22. }
  23. // s1.swap(s2)
  24. void swap(string& s)
  25. {
  26. ::swap(_str, s._str);
  27. ::swap(_size, s._size);
  28. ::swap(_capacity, s._capacity);
  29. }
  30. // 拷贝构造
  31. string(const string& s)
  32. :_str(nullptr)
  33. {
  34. cout << "string(const string& s) -- 深拷贝" << endl;
  35. string tmp(s._str);
  36. swap(tmp);
  37. }
  38. // 赋值重载
  39. string& operator=(const string& s)
  40. {
  41. cout << "string& operator=(string s) -- 深拷贝" << endl;
  42. string tmp(s);
  43. swap(tmp);
  44. return *this;
  45. }
  46. // 移动构造
  47. string(string&& s)
  48. :_str(nullptr)
  49. , _size(0)
  50. , _capacity(0)
  51. {
  52. cout << "string(string&& s) -- 移动构造" << endl;
  53. swap(s);
  54. }
  55. // 移动赋值
  56. string& operator=(string&& s)
  57. {
  58. cout << "string& operator=(string&& s) -- 移动赋值" << endl;
  59. swap(s);
  60. return *this;
  61. }
  62. ~string()
  63. {
  64. delete[] _str;
  65. _str = nullptr;
  66. }
  67. char& operator[](size_t pos)
  68. {
  69. assert(pos < _size);
  70. return _str[pos];
  71. }
  72. void reserve(size_t n)
  73. {
  74. if (n > _capacity)
  75. {
  76. char* tmp = new char[n + 1];
  77. strcpy(tmp, _str);
  78. delete[] _str;
  79. _str = tmp;
  80. _capacity = n;
  81. }
  82. }
  83. void push_back(char ch)
  84. {
  85. if (_size >= _capacity)
  86. {
  87. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
  88. reserve(newcapacity);
  89. }
  90. _str[_size] = ch;
  91. ++_size;
  92. _str[_size] = '\0';
  93. }
  94. //string operator+=(char ch)
  95. string& operator+=(char ch)
  96. {
  97. push_back(ch);
  98. return *this;
  99. }
  100. const char* c_str() const
  101. {
  102. return _str;
  103. }
  104. private:
  105. char* _str;
  106. size_t _size;
  107. size_t _capacity; // 不包含最后做标识的\0
  108. };
  109. }

我们发现,当我们调用push_back与emplace_back插入一样的左值和右值,插入时调用的拷贝构造与构造函数差不多,没有任何的区别,但是当我们换种方式插入时,二者的区别就显示出来了!

如上所示,为什么我们直接插入一个字符串时,push_back是构造+移动构造,而emlpace_back只是构造呢???

首先:因为我们是list<MKL::string> ; 所以此时list的push_back只支持插入左值(左值是MKL::string类型的),右值(右值也是MKL::string类型的)!!!

而我们此时插入的是字符串,而push_back此时只能插入MKL::string类型的,所以我们需要先将其隐式类型转换,构造一个MKL::string类型的临时对象(匿名对象),而此时的临时(匿名)对象是具有右值属性的,所以再调用移动构造将资源转移,就完成插入,所以是一次构造+移动构造!!!

而emplace系列是支持可变参数模版的,所以此时emplace_push将这个字符串识别成const char* 类型,然后一路向下传递,完美转发,一直传到allocator_traits::construct函数中,然后再allocator_traits::construct函数中使用定位new,调用构造函数对传过来的const char*构造对象!!

综上,我们可以看出,在这样深拷贝的类中,调用push_back,与emplace_back还是有一定差别的,一个是构造+移动构造,一个是直接构造,但是这种差别并不是特别大,效率也不是提升那么高!!!

其实除了上面效率的些许差别,其实push_back与emplace_back在用法(写法)上也有点差别,具体如下所示:

所以,emlpace系列的插入接口确实要比之前的插入接口的效率要好一点,但是也是分情况的,而push_back等插入接口所能做的emplace系列都能做到,所以建议无脑使用emplace系列的插入接口也未必不可!!!

其实可变参数模板除了在STL容器中的emplace系列插入接口中有所体现,其实在线程中也体现出可变参数模板的作用!!!

但是这里的可变参数模版在这里起到什么作用,我们后面在详细的说明吧,这里先认识一下!!

二:lambda表达式

           2.1:见一见lambda表达式

Lambda表达式是一种函数的匿名表达方式,可以用来创建匿名的函数对象,Lambda表达式可以代替冗长的函数定义,使代码更加简洁。同时能够更直观地表达函数的功能,增强了代码的可读性!下面给出一个例子,先见一见lambda表达式:

  1. //学生类
  2. struct students
  3. {
  4. string _name; // 名字
  5. double _result; // 分数
  6. int _classid; // 班级
  7. students(const char* str, double result, int classid)
  8. :_name(str)
  9. , _result(result)
  10. , _classid(classid)
  11. {}
  12. };

这里给出一个学生类,类中有学生的成绩,如果我们想对这些学生的成绩进行升序/降序来排序,该如何做呢?

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法;

当然,这里我们要排序的是自定义的类,那么我们需要自己定义比较规则,std::sort在比较的时候,是模板,所以一般需要我们传入一个可调用的对象,即函数指针或者仿函数对象!!!

相较于函数指针,仿函数来实现升序/降序更加灵活,方便,下面我们用仿函数来完成上述要求:

  1. //升序:
  2. struct CompareResultLess
  3. {
  4. bool operator()(const students& sl, const students& sr)
  5. {
  6. return sl._result < sr._result;
  7. }
  8. };
  9. //降序:
  10. struct CompareResultGreater
  11. {
  12. bool operator()(const students& sl, const students& sr)
  13. {
  14. return sl._result > sr._result;
  15. }
  16. };

通过仿函数我们确实可以完成上述要求的排序,但是随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,同时,有可能仿函数和调用的地方距离较远,不利于提高代码的可读性,这些问题都给编程者带来了极大的不便,所以在C++11中,出现了lambda表达式;下面我们用lambda表达式来完成上述的排序要求!!!

  1. //学生类
  2. struct students
  3. {
  4. string _name; // 名字
  5. double _result; // 分数
  6. int _classid; // 班级
  7. students(const char* str, double result, int classid)
  8. :_name(str)
  9. , _result(result)
  10. , _classid(classid)
  11. {}
  12. };
  13. int main()
  14. {
  15. //列表初始化
  16. vector<students> v = { { "大大怪", 72.2, 5 }, { "小小怪", 83.3, 4 }, { "开心", 94.4, 3 }, { "小心", 61.1, 4 } };
  17. sort(v.begin(), v.end(), [](const students& sl, const students& sr)->bool {return sl._result < sr._result; });
  18. for (auto& e : v)
  19. {
  20. cout << e._name << ":" << e._result << endl;
  21. }
  22. cout << "-----------------------------------------------" << endl;
  23. sort(v.begin(), v.end(), [](const students& sl, const students& sr)->bool {return sl._result > sr._result; });
  24. for (auto& e : v)
  25. {
  26. cout << e._name << ":" << e._result << endl;
  27. }
  28. }

如上所示,这就是使用lambda表达式来解决上述问题,和仿函数相比,我们可以看出,使用lambda表达式不需要在其他地方写比较规则,直接在要调用的地方给出,使阅读代码的人一看到lambda表达式就知道本次排序的比较规则是什么样子的,大大提高了代码的可读性!!!

 2.2:lambda表达式的语法规规则

lambda表达式书写格式[capture-list] (parameters) mutable -> return-type { statement}
lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:异变,默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 但是该lambda函数不能做任何事情,没有什么意义!

  1. int main()
  2. {
  3. //最简单的lambda表达式:[]{};
  4. auto add1 = [] {};//用auto来接收这个lambda对象,lambda表达式的本质就是生成一个匿名对象
  5. }

/// 

  2.3:lambda表达式的捕捉列表

在lambda表达式中,参数列表,返回值,函数体是我们的老熟人了,毕竟函数的组成部分不就是它们三个嘛,所以我们重点认识一下lambda表达式的捕捉列表和异变!!!

在认识捕捉列表和异变之前,下面我们先写一个两数交换的例子,再熟悉熟悉lambda表达式的写法:

  1. int main()
  2. {
  3. int x = 10;
  4. int y = 20;
  5. cout <<"x= "<< x << " " <<"y= "<< y << endl;
  6. auto swap = [](int& rx, int& ry){int temp = rx; rx = ry; ry = temp; };
  7. swap(x, y);
  8. cout <<"x= "<< x << " " <<"y= "<< y << endl;

这是正常的lambda表达式的写法,用auto去接收lambda表达式所生成的匿名对象,然后再调用这个匿名对象去完成交换!但是还有一种比较奇怪的写法,具体如下所示:

看起来很奇怪,但是确实能够完成交换,因为看起来比较别扭,我们还是用上面那种方法比较好!!!

下面我们来认识认识捕捉列表和异变;

首先,我们发现,当我们要完成两数交换,写lambda表达式的时候,我们竟然要传参数!!!这是为什么?明明lambda表达式和参数在同一个main函数下呀,下面,我们不传参看看能不能交换成功,具体如下所示:

我们可以看到,先别说能不能完成交换,现在连程序都直接崩了!!!因为lambda表达式的函数体是和main函数是不同作用域的,它不像main函数和if,else那样,是局部域和整个作用域那样的关系,lambda表达式的作用域和main函数的作用域是完全独立的,就像main函数与另一个函数一样,彼此之间相互独立,只不过lambda表达式的作用域看起来恰巧在main函数作用域内!

此时,捕捉列表就派上用场了,当我们不像手动的传参,也想在函数体内使用main函数作用域内的参数,此时我们可以通过捕捉列表来捕捉上下文中的变量供其使用。具体如下所示:

可是我们看到,就算我们使用捕捉列表去捕捉参数,但是还不能用!!!-----------因为这样捕捉方式被称为“传值捕捉”!!!

传值捕捉所捕捉的参数是外面参数的临时拷贝,我们无法改变,如果想使传值捕捉能运行!我们可以使用mutable(异变)!!!具体如下所示:

可以看出,即使我们使用mutable(异变)让传值捕捉运行成功,但是还达不到我们交换的要求,因为传值捕捉就是函数的传值传参一样,形参的改变不会实参!!!

那么根据我们以往写函数的经验,传值传参不会影响实参,那么我们就会直接传引用传参,将实参传递过去,那么lambda表达式的捕捉列表是不是也是这个意思呢?让捕捉列表捕捉到实参呢?具体如下所示:

确实如我们所料,如果我们将实参传递过去,那么此时即可完成交换!而这样捕捉方式,我们称之为“引用捕捉”!!!

但是我们可不能和取地址搞混淆了,因为平常我们表示引用时,前面都需要加类型(如int& x),不加类型的我们都当作取地址(如:&x),但是在lambda表达式中,我们需要将其(&x)当作引用!!!

同时,lambda表达式不仅仅只有传值捕捉和传引用捕捉,还有混合捕捉,全引用捕捉,全传值捕捉,等捕捉方式,这里我就不一一介绍,一并给出:

捕获列表总结:
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[value]:表示值传递方式捕捉变量value
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&value]:表示引用传递捕捉变量value
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
a. 上面所说的父作用域指包含lambda函数的语句块!
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割!比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,其他所有变量但是传值捕捉!
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误!比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a就重复了,这样是不允许的!

2.4:lambda表达式的底层原理

其实在编译器底层看来,根本就没有什么lambda表达式,lambda表达式都会被编译器处理成“仿函数”!!!就像范围for和迭代器一样,其实根本上就没有范围for,范围for所实现的自动迭代,底层都是依靠迭代器完成的!lambda表达式也一样的,编译器根据你写的lambda表达式生成了一个仿函数类!并且生成的这个仿函数类是一个空类,因为lambda表达式的参数列表是捕捉或者外部传递进来的,所以生成的这个仿函数类是没有成员变量的,即lambda表达式的大小为一个空类的大小----->一个字节!!!下面,我们写一个lambda表达式,计算一下它的大小是否为一个字节:

确实如我们所料,lambda表达式的大小和一个仿函数的大小相同,都是一个空类(一个字节)的大小!!!

那么我们怎么证明编译器将lambda表达式处理生成一个仿函数类呢?首先,我们先写一个类,同时再写一个仿函数和lambda表达式,让两者实现同样的效果,具体如下所示:

  1. class Rate
  2. {
  3. public:
  4. Rate(double rate)
  5. : _rate(rate)
  6. {}
  7. //仿函数:
  8. double operator()(double money, int year)
  9. {
  10. return money * _rate * year;
  11. }
  12. private:
  13. double _rate;
  14. };
  15. int main()
  16. {
  17. // 函数对象
  18. double rate = 0.49;
  19. Rate r1(rate);
  20. r1(10000, 2);
  21. // lambda表达式
  22. auto r2 = [=](double monty, int year)->double {return monty * rate * year;};
  23. r2(10000, 2);
  24. return 0;
  25. }

下面我们通过汇编来观察调用仿函数和调用lambda表达式时的底层调用逻辑,具体如下所示:

我们可以看到,lambda表达式的调用过程和仿函数非常相像,都好像调用了构造函数,同时又调用了operator();至于lambda表达式调用operator()时前面的名称其实就是生成的匿名对象的名称,即是lambda+uuid!!!(uuid即通用唯一识别码,主要是生成唯一的临时标识符,以避免lambda表达式所生成的匿名对象名称冲突和重复)!所以,在编译器看来,根本就没有lambda表达式,lambda表达式其实就是仿函数!!!

同时,我们可以写两个lambda表达式,用typeid打印一下它们的名称,看看lambda表达式所生成的匿名对象的名称是否相同: 

正是由于生成的匿名对象的名称是由ambda+uuid组成的,所以每一个lambda表达式所生成的匿名对象的名称就不相同名称不同那么就导致类型就不同!就使得lambda表达式之间不能相互赋值!!!

三:包装器

         3.1:function包装器

        function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器,为什么要有这个function包装器呢?因为在我们之前的学习过程中可调用对象太多了,如函数、函数指针、成员函数、lambda表达式等,这些可调用对象不仅仅调用方式各不相同,而且有些写起来也比较麻烦,所以C++11中引入了function模板类作为函数对象包装器,它是一个通用的函数封装器,可以存储、复制和调用任意可调用对象!!!function包装器的定义如下所示:

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

那么下面我们来看看为什么需要function包装器,我们先用函数指针,仿函数,lambda表达式来各种实现两数相加,具体如下所示:

我们看到,不同的方法导致调用方式大不不同,那么我们能不能用统一的方法调用这些可调用对象呢?下面我们用function包装器来解决:

此时我们看到,当使用function包装器时,本质上还是将这些可调用对象再封装!将他们封装成同一个类型,但是,调用时,还是各自调用各自的,

这样,我们就只需要在定义时提供被包装的函数或函数对象的类型,然后可以像普通函数一样调用它。这就是function包装器的包装功能!!!

同时,再使用function包装器时我们需要注意一些事项:
首先,使用function包装器就必须先包含<functional>这个头文件,

其次,我们需要在定义的时候提供被包装函数的信息(主要是返回值,和参数),因为返回值和参数决定着function包装器中主要信息的填充!

函数指针,仿函数,lambda表达式如何通过function包装器进行包装我们已经提过了,那么成员函数也算是可调用对象,那么它该如何包装呢?
众所周知,成员函数分为两种,静态成员函数和非静态成员函数,那么二者包装时有什么差异呢?下面,我们给出一个类,类中包含静态成员函数和非静态成员函数这两个成员变量,具体如下所示:

  1. class Plus
  2. {
  3. public:
  4. Plus(int n = 3)//构造函数
  5. :_n(n)
  6. {
  7. }
  8. static int plusi(int a, int b)//静态成员函数
  9. {
  10. return a + b;
  11. }
  12. double plusd(double a, double b)//非静态成员函数
  13. {
  14. return (a + b) * _n;
  15. }
  16. private:
  17. int _n = 3;
  18. };

下面我们对其进行包装:

  1. int main()
  2. {
  3. //包装类内的静态成员函数,可以直接正常包装就行(但是需要指定类域);
  4. //function<int(int, int)> f1 = Plus::plusi;
  5. //普通成员函数却不能正常的包装,会报错,如果想包装,那么需要在需要在要包装的成员函数之前+&!--这是语法规定!!!
  6. //function<double(double, double)> f2 = &Plus::plusd;
  7. //既然普通成员函数需要+&。那么静态成员函数也可以在前面+&---所以通常我们包装成员函数时一般都在前面+&!!!
  8. function<int(int, int)> f1 = &Plus::plusi;//静态成员函数也可以+&
  9. //但是普通成员函数+&这样还是包不了,因为非静态成员函数除了形参还有一个隐藏参数---this指针!!
  10. //function<double(double, double)> f2 = &(Plus::plusd);//会报错!
  11. function<double(Plus, double, double)> f2 = &(Plus::plusd);//加上this指针主要是加Plus(类名)时,才能真正的包装成功!!!
  12. cout << f1(10, 20) << endl;//第一种包装静态成员变量正常调用就行!
  13. cout << f2(Plus(), 100, 200) << endl;//非静态成员包装后,在调用的时候需要加上一个匿名对象!
  14. cout << f2(Plus(2), 100, 200) << endl;//也可以给这个匿名对象赋值(前提得有构造函数!)
  15. //同时,我们当然也可以使用有名对象调用!
  16. Plus p1(10);
  17. cout << f2(p1, 10, 40) << endl;
  18. }

这就是function包装器包装成员函数(静态成员函数/非静态成员函数)的方式!!!但是我们发现,当包装非静态成员函数时,需要传this指针,而我们只是传了类名(Plus)即可,为什么不是Plus*呢???

因为当我们传Plus时,此时是用Plus这个Plus对象调用的非静态成员函数,而当我们传Plus*时,是这个Plus*指针在调用非静态成员函数!!!

如果是传Plus*,那么我们就不能使用匿名对象调用了,因为匿名对象是右值,而Plus*时,需要对对象取地址,但是右值是无法取地址的,即此时就无法用匿名对象调用非静态成员函数了!!!

所以通常我们都是直接传类名(Plus),因为这样我们即可以用匿名对象,又可以用有名对象调用类中的非静态成员函数!!!

综上,包装器其实就是适配器,就和我们之前学习的容器适配器很相似,它的底层并没有存储数据和管理数据的能力,它只是包装,实际上的管理与调用执行还是靠被包装的对象来做!!!

那function包装器的具体应用是什么样子的呢???下面我们给出一道题:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

这道逆波兰表达式题目的大致解题思路:
首先,定义一个栈,其次,遍历题目给的字符串,如果不是运算符则转成数字入栈,如果是运算符,则取出栈顶的两个元素,进行运算,然后将结果入栈,最后留在栈顶的数字就是最终的结果,其具体代码实现如下所示:

  1. int evalRPN(vector<string>& tokens)
  2. {
  3. stack<int> sk;//定义一个栈
  4. for(auto& str : tokens)//遍历字符串
  5. {
  6. //如果是运算符,则取出栈顶两个元素,进行运算,并将其结果再放入栈中
  7. if(str=="+"||str=="-"||str=="*"||str=="/")
  8. {
  9. int right=sk.top();
  10. sk.pop();
  11. int left=sk.top();
  12. sk.pop();
  13. switch(str[0])
  14. {
  15. case '+':
  16. sk.push(left+right);
  17. break;
  18. case '-':
  19. sk.push(left-right);
  20. break;
  21. case '*':
  22. sk.push(left*right);
  23. break;
  24. case '/':
  25. sk.push(left/right);
  26. break;
  27. }
  28. }
  29. else//如果不是运算符,则直接将其转成数字入栈!
  30. {
  31. sk.push(stoi(str));
  32. }
  33. }
  34. return sk.top();//最后,栈顶的元素就是最终结果!
  35. }

但是我们可以看到,运算符(+-*/)都是同样类型的参数和返回值,那么我们能够用统一的方式去调用这些运算符所代表的各种运算呢???

  1. int evalRPN(vector<string>& tokens)
  2. {
  3. stack<int> sk;
  4. map<string,function<int(int,int)>> mapfunc=
  5. {
  6. {"+",[](int x,int y)->int{return x+y;}},
  7. {"-",[](int x,int y)->int{return x-y;}},
  8. {"*",[](int x,int y)->int{return x*y;}},
  9. {"/",[](int x,int y)->int{return x/y;}}
  10. };//这里不一定只能是lambda表达式,也可以写函数,仿函数等,可变,灵活!
  11. for(auto& str : tokens)
  12. {
  13. if(mapfunc.count(str))
  14. {
  15. int right=sk.top();
  16. sk.pop();
  17. int left=sk.top();
  18. sk.pop();
  19. sk.push(mapfunc[str](left,right));
  20. }
  21. else
  22. {
  23. sk.push(stoi(str));
  24. }
  25. }
  26. return sk.top();
  27. }

我们可以将各种运算符和运算符对应的操作通过建立KV关系映射到一起,这样的话,如果想执行某一个运算符所对应的操作,只需要在KV关系中找到对应的运算符即可!!同时当我们想再添加一下运算符操作时,就不必在switch语句中添加了,直接在KV模型中添加,使代码更加的简洁与高效!所以这种通过建立映射模型+function包装器的方法,在网络中有很大的作用(通过某些指令去做某些操作)!!

  3.2:bind(绑定)

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

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

bind主要有两种用法:

  3.2.1:调整参数顺序:

调整参数顺序时,调用bind的一般形式:auto newCallable = bind(callable,placeholders_n);

其中,newCallable本身是一个可调用对象,placeholders是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它placeholders中的参数。placeholders中的参数可能包含形如_n的名字,其中n是一个整数,

这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推,具体如下所示:

  1. //bind包装器:
  2. //1:调整参数顺序:
  3. void print(int a, int b)
  4. {
  5. cout << a << " " << b << endl;
  6. cout << "void print(int a, int b)" << endl;
  7. }
  8. int main()
  9. {
  10. print(10, 20);
  11. //占位符:placeholders,后面跟着的_1,_2等就是函数的形参,_1(第一个形参),_2(第二个形参)。。。。。。
  12. auto rprint=bind(print, placeholders::_2 , placeholders::_1);
  13. rprint(10, 20);
  14. //不仅仅可以用auto来自动推导返回类型,也可以使用包装器来包装函数
  15. function<void(int, int)> rprint1 = bind(print, placeholders::_2, placeholders::_1);
  16. rprint1(10, 20);
  17. return 0;
  18. }

这里std::placeholders::_1就是print函数的第一个参数,std::placeholders::_2则是print的第二个参数,但是我们在这里将第一个参数和第二个参数进行了互换,​​​​

但是这种只是调整参数顺序感觉在实际中并没有多大的用场,这里我们只需要记住std::placeholders这个占位符的作用即可!!!

3.2.2:调整参数个数

有时候我们在调用类内的成员函数时,有什么我们需要调用非静态成员函数,那么在调用时,我们就要使用隐藏的this指针,而正是由于这个隐藏的this指针,使得调用时我们需要多传递一个参数,比较麻烦,那么此时,我们就可以通过bind来调整参数个数,将this指针绑定住,那么我们就可以像调用普通的函数那样调用类中的非静态成员函数了,具体如下所示:

  1. 2:调整参数个数:
  2. class Sub
  3. {
  4. public:
  5. Sub(int rate)
  6. :_rate(rate)
  7. {
  8. }
  9. int my_sub(int a, int b)
  10. {
  11. return _rate * (a - b);
  12. }
  13. private:
  14. int _rate;
  15. };
  16. int main()
  17. {
  18. //包装成员函数:
  19. function<int(Sub, int, int)> f1 = &Sub::my_sub;
  20. cout << f1(Sub(30), 30, 20) << endl;//但是包装完调用时,我们需要绑定三个参数,而我们要调用的函数参数只有两个呀!
  21. cout << "----------" << endl;
  22. //所以bind可以调整参数个数,在定义函数时绑定固定的值!
  23. //绑定任意一个参数,那么等调用的时候就不需要显示的给这个参数传了;因为它已经在函数定义时就被确定下来了!
  24. //绑定第一个参数;把第一个参数绑死:Sub(30),其他的使用占位符,等待传参;
  25. function<int(int, int)> f2 = bind(&Sub::my_sub, Sub(30), placeholders::_1, placeholders::_2);
  26. cout << f2(30, 20) << endl;//此时调用时就不需要显示的传第一个参数了,那么此时我们就只需要绑定两个参数!
  27. //绑定第二个参数;
  28. function<int(Sub, int)> f3 = bind(&Sub::my_sub, placeholders::_1, 30, placeholders::_2);
  29. cout << f3(Sub(30), 20) << endl;//此时调用时就不需要显示的传第二个参数了,
  30. //绑定第三个参数;
  31. function<int(Sub, int)> f4 = bind(&Sub::my_sub, placeholders::_1, placeholders::_2, 20);
  32. cout << f4(Sub(30), 30) << endl;//此时调用时就不需要显示的传第三个参数了,
  33. return 0;
  34. }

 这里我们需要注意占位符大小,比如拿绑定第二个参数为例,此时第一个参数的位置是占位符placeholders::_1,这个毫无问题,但是第三个参数的位置是占位符placeholders::_2!!!而不是placeholders::_3!因为语法规定,当你bind时,绑死了每一个参数,使参数减少,但是此时占位符是从小到大依次使用,不会跳跃使用,也不会因为你在哪一个位置占位符就是几!

综上:bind(绑定)的作用:

1:将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用显示的传递,使传参的个数减少(比较有用)。

2:可以对函数参数顺序进行灵活调整(但是不是很实用)。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/577067
推荐阅读
  

闽ICP备14008679号