C++ 可调用对象

在使用一些基于范围的模板函数时(如 sort()、all_of()、find_if() 等),常常需要我们传入一个可调用对象,以指明我们需要对范围中的每个元素进行怎样的处理。可调用对象可以是函数函数指针lambda表达式bind创建的对象或者重载了函数调用运算符的类对象

函数

bool cmp(const int &a, const int &b) {
	return a < b; // 从小到大排列
  //return a > b; // 从大到小排列
}

vector<int> v = {3, 6, 2, 7, 4, 9, 1};
sort(v.begin(), v.end(), cmp);

函数指针

bool cmp(const int &a, const int &b) {
	return a < b; // 从小到大排列
  //return a > b; // 从大到小排列
}

bool (*p)(const int &a, const int &b);//创建一个函数指针
p = cmp;//与函数进行绑定

vector<int> v = {3, 6, 2, 7, 4, 9, 1};
sort(v.begin(), v.end(), p);

lambda表达式

使用函数或者函数指针充当可调用对象对函数的形参有严格的限制,有时我们想传入一些辅助变量,使用函数充当可调用对象已无法满足这一需求,而 lambda 表达式中的捕获列表(parameter list)可以帮助我们很容易做到这一点。

lambda 表达式具有如下形式:
[caputure list](parameter list) -> return type { function body }

  • caputure list :一个 lambda 所在函数中定义的局部变量的列表。
  • parameter list:参数列表,需要传入的形参。
  • return type:lambda 表达式的返回类型
  • function body:lambda 表达式的函数体

一个简单的 lambda 表达式的例子:

auto f = [] { return "hello world"; }; //注意:不要漏掉最后的分号
cout << f() << endl; // 输出:hello world

注意:lambda 表达式可以省略参数列表(如果 lambda 表达式不需要传入任何形参),也可以省略返回类型,但不可以省略捕获列表和函数体。

使用捕获列表

下面的函数 larger 判断一个数组中的元素与指定的 base 的大小关系。

void larger(int base, vector<int> v) {
	for_each(v.begin(), v.end(), [base](const int &num){cout << (num > base);});
} 

类似函数传参,上面使用的是值捕获,可以是使用引用捕获
[&sz]{sz = 1};

隐式捕获
除了像上面那样明确指出我们需要捕获的变量列表外,我们还可以使用隐式捕获,即直接在函数体内使用我们需要使用的辅助变量而不用在捕获列表中声明,但需要指出捕获方式——值捕获 or 引用捕获。

void larger(int base, vector<int> v) {
    //隐式值捕获
	for_each(v.begin(), v.end(), [=](const int &num){cout << (num > base);});
    //隐式引用捕获
  //for_each(v.begin(), v.end(), [&](const int &num){cout << (num > base);});
} 

隐式捕获和显示捕获还可以混合使用
[=, identifier_list] // identifier_list中不得再出现值捕获
[&, identifier_list] // identifier_list中不得再出现引用捕获

可变lambda

通常情况下,我们不能在 lambda 函数体中改变捕获变量的值(即使是采用值捕获的方式也不行)。如果我们确实有这种需求,可以在函数体前添加 mutable 关键字接触这一限制。

int a = 1;
auto f = [a] () mutable { return ++a; };
int b = f(); // a = 1, b = 2

指定 lambda 的返回类型

有时编译器难以自动确定 lambda 表达式的返回类型,需要我们明确指定。下面的例子使用 transform 算法将 v 中的数组取绝对值。

vector<int> v = {1, -2, -3, 4}
transform(v.begin(), v.end(), v.begin(), 
          [](int i) -> int {
              if (i < 0)
                  return -i;
              else 
                  return i;
          });// v = {1, 2, 3, 4}

bind 创建的对象

lambda 表达式适合在对元素进行一些简单的处理,在 lambda 表达式中编写冗长的代码会使得代码风格变得很糟糕。进行复杂处理还是使用函数更为合适,但还是那个问题需要解决:算法模板对传入的参数有严格的数量限制,而有时我们需要传入一个额外的参数给处理函数。使用 bind 将处理函数进行一次包装可以很好地解决这一问题。

例如,对于 lambda 表达式[base](const int &num){cout << (num > base);},我们可以编写一个等价的函数

void func(int num, int base) {
    cout << (num > base);
}

但我们却不能将这个 func 直接传入 for_each() 算法模板中。使用 bind 创建一个可调用对象

void func(int num, int base) {
	cout << (num > base);
}

void main() {
	vector<int> v = {1, 2, 3, 4, 5};
	int base = 3;
	auto f = bind(func, _1, base); 
	for_each(v.begin(), v.end(), f); // 输出:00011
}

bind函数的参数列表含义:
bind(function, arg_list)

其中 function 为要包裹的函数,arg_list 与 function 的参数列表按序逐一对应,但其中的有一类特殊的参数,形如 _1_2_3等。这些参数即指供算法模板(即这里的 for_each())使用的参数,而那些没有使用这种的形式的参数便相当于 lambda 捕获列表中的参数。 _n表示这是供算法模板使用的第 n 个参数(从1开始计数)。

bind 函数定义在 functional 头文件中,_n定义在 std::placeholders 命名空间下。使用 bind 需要:

#include<functional>
using namespace std::placeholders

重载函数调用运算符的类

当对处理函数非常复杂时,我们可以使用一个实现了调用运算符的类的对象来充当可调用对象。在构造类的对象时将需要捕获的参数传入,而类实现的调用函数的形参需要符合算法模板的要求。这是一种实现复杂但却功能最为强大的终极解决方案。

class Bigger {
private:
	int base_;
public:
	Bigger(int base) {
		base_ = base;
	}
	void operator() (const int &num) {
		cout << (num > base_);
	}
};

void main() {
	vector<int> v = {1, 2, 3, 4, 5};
	int base = 3;
	Bigger bigger(base);
	for_each(v.begin(), v.end(), bigger);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章