【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不断变大。


Fox!
+关注
目录
打赏
0
0
0
0
2
分享
相关文章
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
67 2
【C++11】lambda表达式
C++11 引入了 Lambda 表达式,这是一种定义匿名函数的方式,极大提升了代码的简洁性和可维护性。本文详细介绍了 Lambda 表达式的语法、捕获机制及应用场景,包括在标准算法、排序和事件回调中的使用,以及高级特性如捕获 `this` 指针和可变 Lambda 表达式。通过这些内容,读者可以全面掌握 Lambda 表达式,提升 C++ 编程技能。
161 3
C++ 11新特性之Lambda表达式
C++ 11新特性之Lambda表达式
32 0
|
7月前
|
C++一分钟之-泛型Lambda表达式
【7月更文挑战第16天】C++14引入泛型lambda,允许lambda接受任意类型参数,如`[](auto a, auto b) { return a + b; }`。但这也带来类型推导失败、隐式转换和模板参数推导等问题。要避免这些问题,可以明确类型约束、限制隐式转换或显式指定模板参数。示例中,`safeAdd` lambda使用`static_assert`确保只对算术类型执行,展示了一种安全使用泛型lambda的方法。
93 1
|
8月前
|
C++
C++语言的lambda表达式
C++从函数对象到lambda表达式以及操作参数化
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
1月前
|
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
70 19
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等