【C++11】lambda表达式 包装器

简介: 【C++11】lambda表达式 包装器

1 lambda表达式

1.1 引例

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

#include <algorithm>
#include <functional>
int main()
{
  int array[] = { 4,1,8,5,3,7,0,9,2,6 };
  // 默认按照小于比较,排出来结果是升序
  std::sort(array, array + sizeof(array) / sizeof(array[0]));
  // 如果需要降序,需要改变元素的比较规则
  std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
  return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

struct Goods
{
  string _name;  // 名字
  double _price; // 价格
  int _evaluate; // 评价
  Goods(const char* str, double price, int evaluate)
    :_name(str)
    , _price(price)
    , _evaluate(evaluate)
  {}
};
struct ComparePriceLess
{
  bool operator()(const Goods& gl, const Goods& gr)
  {
    return gl._price < gr._price;
  }
};
struct ComparePriceGreater
{
  bool operator()(const Goods& gl, const Goods& gr)
  {
    return gl._price > gr._price;
  }
};
int main()
{
  vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
   3 }, { "菠萝", 1.5, 4 } };
  sort(v.begin(), v.end(), ComparePriceLess());
  sort(v.begin(), v.end(), ComparePriceGreater());
  return 0;
}

如果仿函数命名比较规范的话,像上面的命名方式的话那还好,如果遇到了像cmp1 cmp2 cmp3…这种命名方式而且还没有注释的话可以让人烦死,自己还得去找对应的源码实现,而如果在一个工程中有很多代码,找的代价也会比较大,所以C++11便新推出了一个语法就是lambda表达式。

1.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函数不能做任何事情。

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。


[var]:表示值传递方式捕捉变量var

[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

[&var]:表示引用传递捕捉变量var

[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

[this]:表示值传递方式捕捉当前的this指针

我们可以来实现一个简单的add来验证一下:

int main()
{
  int x, y;
  cin >> x >> y;
  auto add = [=]()
  {
    return x + y;
  };
  cout << add() << endl;
  return 0;
}

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。像上面的add你甚至还可以这样写:cout<< [=](){return x + y;}()<< endl;

我们可以来看看mutable的应用场景,比如下面的代码:

int main()
{
  int x = 10,y = 20;
  auto swapInt = [=] {int tmp = x; x = y; y = tmp; };
  swapInt();
  return 0;
}

当我们编译时会直接报错的:

81c215c0f1dc48bc8672de8f2e0860ce.png

为什么呢?因为我们是用值捕捉的方式捕捉到的变量,而捕捉到的变量是一份拷贝,并且默认是不让你你修改的(可以理解为增加了const属性),所以当你修改变量是会直接报错的,那假如我们想让其修改呢?我们就可以用mutable(意思是易变的):

2dbc046bb0be46a987dfa129b25e84d7.png


注意:

  1. 父作用域指包含lambda函数的语句块。
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
  3. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
  4. 在块作用域以外的lambda函数捕捉列表必须为空。
  5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  6. lambda表达式之间不能相互赋值,即使看起来类型相同。
  7. 前面的注意事项都很好理解,最后一个注意点我们可以验证下:
void (*PF)();
int main()
{
 auto f1 = []{cout << "hello world" << endl; };
 auto f2 = []{cout << "hello world" << endl; };
 //f1 = f2;   // 编译失败--->提示找不到operator=()
 // 允许使用一个lambda表达式拷贝构造一个新的副本
 auto f3(f2);
 f3();
 // 可以将lambda表达式赋值给相同类型的函数指针
 PF = f2;
 PF();
 return 0;
}

注意事项代码中都有注释。

至于为啥不允许赋值,我们后面讲解lambda表达式的原理时会给出解释。

1.3 lambda表达式的底层原理

函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。

我们写一段代码来验证一下:

class Rate
{
public:
  Rate(double rate) : _rate(rate)
  {}
  double operator()(double money, int year)
  {
    return money * _rate * year;
  }
private:
  double _rate;
};
int main()
{
  //  函数对象
  double rate = 0.49;
  Rate r1(rate);
  r1(10000, 2);
  //  lamber
  auto r2 = [=](double monty, int year)->double { return monty * rate * year;};
  r2(10000, 2);
  return 0;
}

c7e1f3ac2a1b407695921848152264f0.png

从汇编的角度来看,我们不难发现lambda表达式在底层也是调用了operator来实现,那为什么lambda表达式不能够相互赋值呢?其本质是因为lambda表达式在底层的命名是采用uuid的方式生成唯一的类名,所以不同类型的对象自然不可以赋值了。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

那考考大家:lambda对象的大小是多少字节呢❓

答案其实已经显而易见了,由于lambda表达式的底层是用仿函数实现的,而仿函数是一个没有内置成员变量的类(空类),大小就是1字节喽,你回答对了吗?

2 包装器

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

使用包装器前我们要引入头文件#include <functional>

类模板的原型如下:

// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

那包装器我们日常是如何使用的呢?

// 使用方法如下:
#include <functional>
int f(int a, int b)
{
  return a + b;
}
struct Functor
{
public:
  int operator() (int a, int b)
  {
    return a + b;
  }
};
int main()
{
  // 函数名(函数指针)
  std::function<int(int, int)> func1 = f;
  cout << func1(1, 2) << endl;
  // 函数对象
  std::function<int(int, int)> func2 = Functor();
  cout << func2(1, 2) << endl;
  // lamber表达式
  std::function<int(int, int)> func3 = [](const int a, const int b)
  {return a + b; };
  cout << func3(1, 2) << endl;
  return 0;
}

我们可以用包装器来接受 函数指针 仿函数 lambda ,这样我们就可以用统一的类型来接受不同的参数,达到只实例化一份的目的。

但是在调用类中非静态成员函数(不包括仿函数)时要额外注意function的语法格式:

比如下面:

class Plus
{
public:
  static int plusi(int a, int b)
  {
    return a + b;
  }
  double plusd(double a, double b)
  {
    return a + b;
  }
};
int main()
{
  // 类的成员函数
  std::function<int(int, int)> func4 = &Plus::plusi;
  cout << func4(1, 2) << endl;
  std::function<double(Plus, double, double)> func5 = &Plus::plusd;
  cout << func5(Plus(), 1.1, 2.2) << endl;
  return 0;
}

我们知道静态成员函数是不包括this指针的,所以用之前的语法是没有问题的,但是由于成员函数有this指针,所以我们就要多给出一个额外的参数对象(我们一般喜欢给匿名对象来调用),通过参数对象来调用里面的成员函数。并且在指定类域是要加上&,这时语法的硬性规定。


72cae5f6d8c64e289919be8efe02d823.png

但是大家注意下面这种调用方式:


54be8b856afe4885b16d891745153220.png

我们也可以用对象指针来调用,但是这时候就不能够用匿名对象了,因为匿名对象是右值,是不能够&的,但是一般情况下我们不会选择这种方式。

3 bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。

一般来说,我们使用bind有下面这两种情况:

  • 1️⃣调换参数顺序
  • 2️⃣改变参数个数
  • 其中调换参数顺序其实一般很少用到,而改变参数个数很有意思,我们接下来一个一个来看:

比如下面这段程序:

void Print(int x, int y)
{
  cout << x << ":" << y << endl;
}

假设我们不改变Print函数的实现,而打印结果时交换参数顺序,我们可以怎么做?

我们可以用bind来处理:

int main()
{
  int x = 10, y = 20;
  Print(x, y);
  auto RPrint = bind(Print, placeholders::_2, placeholders::_1);
  RPrint(x, y);
  return 0;
}

这里面的_1 _2 是什么鬼呀?这其实是封装在placeholders命名空间中的一个占位符,正如我们直接理解的那样, _1 _2 ……分别代表着第一个参数,第二个参数……,我们想要交换哪些参数的位置可以直接通过交换占位符的顺序即可。

交换参数顺序的用法其实比较鸡肋,我们平时一般也不怎么用到,但是改变参数个数的场景我觉得还是比较有意思的,我们接下来看看这种情况:

void mul(double x, double y)
{
  cout<< x * y<<endl;
}
struct fun
{
  fun(double rate)
    :_rate(rate)
  {}
  void mulR(double x, double y)
  {
    cout << x * y * _rate << endl;
  }
  double _rate;
};
int main()
{
  int x = 10, y = 20;
  function<void(double, double)> f1 = mul;
  function<void(double, double)> f2 = [=](double x,double y) {cout<< x * y<<endl; };
  return 0;
}

当我们要求使用跟上面参数一样的格式来接受fun中的mulR时我们直接写是会直接报错的,在上面我们讲解function时已经详细解释了原理,这里就不在多说了。那我们可以通过bind来处理:

function<void(double, double)> f3 = bind(&fun::mulR,f, placeholders::_1, placeholders::_2);

我们可以通过上面的方式来绑定处理,将第一个参数绑定写死,然后我们就可以只用两个参数的包装器来接受了,是不是很妙。当然,我们不仅可以绑死第一个参数,第二个三个n个参数我们都可以通过bind来绑死,值得注意的小细节是不论我们绑死的是第几个参数,我们其他没有被绑定的参数只能从_1不断变大。


目录
相关文章
|
1天前
|
存储 算法 C++
C++11:lambda表达式 & 包装器
C++11:lambda表达式 & 包装器
8 0
|
1天前
|
存储 算法 对象存储
【C++入门到精通】function包装器 | bind() 函数 C++11 [ C++入门 ]
【C++入门到精通】function包装器 | bind() 函数 C++11 [ C++入门 ]
15 1
|
1天前
|
算法 编译器 程序员
【C++入门到精通】 Lambda表达式 C++11 [ C++入门 ]
【C++入门到精通】 Lambda表达式 C++11 [ C++入门 ]
12 1
|
1天前
|
Java 编译器 Linux
【C++11(二)】lambda表达式以及function包装器
【C++11(二)】lambda表达式以及function包装器
|
1天前
|
编译器 C语言 C++
C++ lambda表达式
C++ lambda表达式
|
1天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
17 0
|
1天前
|
C语言 C++
【C++】string类(常用接口)
【C++】string类(常用接口)
21 1
|
1天前
|
C语言 C++ 容器
C++ string类
C++ string类
8 0
|
1天前
|
C++ Linux