当前位置: 首页 > news >正文

C++ - 完美语义(右值引用的中篇) - lambda表达式

前言

之前对右值引用的理解,用使用场景做了详细说明,具体看博客:
C++ - 右值引用 和 移动拷贝-CSDN博客

在 有值引用 当中还有一个 完美转发,请看本篇博客。

完美转发

 我们现在看这个例子:
 

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}
int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

上述当中的 PerfectForward(a); // 左值 当中,a 是一个左值,但是 PerfectForward()这个函数的参数类型是 T&& t   是一个 右值引用,按照上一篇博客当中对右值引用的介绍,右值引用是不能引用左值的,应该会编译报错。

但是,实际上是没有编译报错,编译通过。

 其实,原因是 T&& t  ,当中的T 是模版参数,模版当中的 T&& 已经不是右值引用了,这里的 T&& 叫做万能引用。

所谓万能引用,就是这个参数,就可以接收左值,有可以接收右值。因为此处是一个模版类型,可以传任意类型的变量进来,不像上篇当中写的一样,对于 左值引用 和 右值引用 是显示写出来的类型,是写死的类型。比如 : int&& ,double&& 都是右值引用。

也就是说,对于万能引用:

  • 传入的模版实参是 左值,他就是左值引用(引用折叠)。 而所谓引用折叠就是,如果传入的实参是 左值的话,那么 T&& 就会 折叠为 T&。
  • 传入的模版实参是 右值,它就是右值引用。也就是 T&&。

 也就是说  : PerfectForward(10);  和   PerfectForward(a);    这两个函数不是同一个函数,他们是同一个模版实例化出来的两个函数,前者是 右值引用版本 ,后者是 左值引用版本。

了解 万能引用 之后,我们来看上述例子的输出:
 

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}
int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

输出:

左值引用
左值引用
左值引用
const 左值引用
const 左值引用

我们发现,输出的结果有点怪,前三个 传入的参数不管是 左值 还是 右值,都是调用的左值的func()函数。

 结果验证,发现,并不是全部都给 引用折叠了,我们直接把 PerfectForward ()函数的参数控制为 右值引用,发现,还是调用的左值引用版本的 func()函数
 

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }void PerfectForward(int&& t)
{Fun(t);
}
int main()
{PerfectForward(10); // 右值int a;PerfectForward(std::move(a)); // 右值return 0;
}

输出:
 

左值引用
左值引用

我们先来看这个小例子:

int a = 1;
int& r = a;
int&& rr = move(a);

需要注意的是,上述 的 r 和 rr 都是左值,move(a)这个表达式是 右值,右值引用的 功能是右值引用,但是属性不是右值,而是左值,因为 右值引用支持修改。而且理论上右值引用必须支持修改,之前的移动拷贝,就是要修改 有值引用当中的数据,和 另一个对象当中的数据进行交换

 比如在 string 当中使用的移动拷贝:

 我们使用 swap ()函数来进行交换两个string对象当中的数据,但是 swap()函数的参数是 string& 是一个 非const 的左值引用,是不能接受右值的,如果 string()拷贝构造函数当中的 s 有值引用的属性是左值的话, swap()怎么可能接受呢?

 所以,看懂上述这个小例子,你应该就清楚,为什么上述 PerfectForward()函数不管传入左值还是有值,调用的都是 左值版本的func()了,因为右值引用的 功能是右值引用,但是属性不是右值,而是左值

 完美转发的使用

 那么,如上述例子,我们在模版当中,模版类型 的&& ,是万能引用,但是不管是 左值引用还是 右值引用,他们的属性都是左值,也就是说,如果在模版函数,或者说 模版类当中的成员函数当中,使用模版类型的形参 调用某个函数的话,只能是左值引用参数类型的函数。

但是,右值引用的优化,在很多地方都会用到,在模版函数,类模版的成员函数当中,也是非常多用的。所以,这时候就要使用完美转发了。

 完美转发的使用方式 跟 move ()类似,使用 forward<模版参数>(模版类型)的方式来使用,如上述例子,应该这样修改:

template<typename T>
void PerfectForward(T&& t)
{// 完美转发Fun(forward<T>(t));
}

 也就是说,上述的 t 这个变量,如果是 左值引用,就保持左值属性;如果是右值引用,就保持右值属性。

完整代码:
 

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T>
void PerfectForward(T&& t)
{Fun(forward<T>(t));
}
int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

输出:
 

右值引用
左值引用
右值引用
const 左值引用
const 右值引用

我们发现,结果就符合我们的预期了。


在类模版当中比如写了一个函数的 左值引用版本 和 右值引用版本,但是又想只用一个的话,不能直接把 某一个删掉:
 

template<class T>
struct ListNode
{T _val;ListNode* _left;ListNode* _right;listNode(const T& x):_val(x),_left(nullptr),_right(nullptr){}listNode(T&& x):_val(forward(x)),_left(nullptr),_right(nullptr){}}

 比如上述,如果把左值引用的函数删除掉的话,那么就会编译报错,左值版本的 insert()和 push_back()当中使用的左值版本的 结点构造函数找不到了。

因为使用 完美转发有一个前提,就是 使用 的 模版参数是推导出来的,而不是示例化出来的。

 函数模版当中的模版参数就是 推导出来的,而且 类模板的模版参数是我们在外部显示传入,也就是显示示例化的。

 如,我们之前的例子,上述的 T 模版参数,是通过 函数的形参 t 推导出来的。

 而,上述的list结点结构体当中的构造函数当中使用的模版参数是 类模板的模版参数,不是推导的,是我们在定义 list<T>  的时候传入的参数,已经定好了。

如果实在想要像上述一样,只写一个的话,就要在类模板当中加一个这个函数的函数模版:

 完美转发的意义 

 我们上述说明了 右值引用的两种使用场景:
首先是 移动语义,也就是移动构造和 移动拷贝,在一定语义当中,因为需要对某一些空间的替换,移动,所以,右值引用的对象要可以修改,所以此时右值引用的属性是左值的,我们可以取地址对其中的 成员进行修改,所以这也是 右值引用的属性是 左值的一大原因。

但是在上述我们说的例子当中,在模版函数和 类模版的成员函数当中是用 模版类型的形参,调用某一函数的时候,如果不使用移动语义就只能是 左值引用版本的函数,这肯定不是我们期望的。

右值引用带来了这么大的性能提升,我们为什么不用呢?

那么,此时我们期望右值引用的属性是左值,但是不能直接替换,因为在上述的移动拷贝当中,要使用 左值的属性。

所以,这时候就有了完美转发的出现了。

 如果像是移动拷贝这种 右值引用 一定要是 左值属性的,那么就直接使用右值引用即可;如果是想要继承 原本 左/右值引用的 对象的 属性(是左值引用属性就是左值,是右值引用属性就是右值),那么就使用完美转发的继承  左/右值引用 原本的属性即可。

 而,移动构造,移动拷贝,并不是延长了某一个变量的生命周期,而是把 上一个变量的空间,转移到另一个 变量当中去了,你可以理解为 这个空间的生命周期延长了,但是上一个变量的声明周期没有延长,但是单独的空间是没有声明周期这个概念的,这个概念是在变量当中的。


 在C++11 之后,每一个容器当中的 insert()和 push_back()函数都进行了 对 右值引用版本的实现,升级。那么在 push_back()函数当中我们一般是 复用 insert()函数就够用了的,但是如果在 右值引用版本的 push_back()复用 insert()函数的话,我们肯定是期望复用 右值引用版本的 insert()函数,那么我们不能直接使用 push_back()传入的 右值引用形参来调用 insert()函数了,因为 右值引用默认是 左值属性,所以这时候我们就要使用 forward 完美转发来强行把 这里的 右值引用函数的属性转化为 左值的属性。

而且,在一个类当中,这个右值引用可能会多次传,比如上述 从 push_back()函数传入到 insert()当中,可能要经历多个函数的传递,为了保证传递之后,还是右值的属性,在每一次传的时候都要进行 完美转发:
 

 传的每一次都需要 完美转发才能继承上一个 属性。

lambda表达式

lambda 表达式的出身,和仿函数的比较

 lambda表达式,可以说是用来替代 函数指针的,还可以替代一些仿函数的功能。

如果我们想要对 某一种比较方法进行替换的话,可以使用仿函数的方式来实现。

像是在 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;
}

 如上述所示,在库当中有两个仿函数,一个是less,一个是 greater;因为仿函数是用一个类当中的 operator()()运算符重载,来解决,普通operator()()同参数不能重载的问题。

像上述一样,我们就可以传入 less 或者 是 greater 类的 匿名对象来控制 sort()函数是排升序还是排降序了。

而,仿函数除了实现 比较顺序的选择,还可以自定义比较规则,比如在一个类当中,有 名字,价格 ,评价分数。那么我们可以先三个仿函数,分别可以按照 名字的 string类进行比较,还可以按照 价格的大小,或者评价分数的大小来进行比较。

比如我们自己来实现下述类的排序:

#include <functional>
#include <algorithm>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());
}

像上述就是使用仿函数的方式,实现,goods 类当中三个成员的三种比较方式。

上述只是仿函数最基本的用法,在哈希表 的 多种类型的 取模;unordered_map 和 unordered_set 两个容器在 底层哈希表当中吗,去 key 值;等等 都是可以用仿函数去实现的。

仿函数最喜欢的适用场景就是 在模版参数当中进行调用,当前模版实例化出的对象,需要什么类型的仿函数,那么可以在外部 显示传入 这个 仿函数的类型。无论是 程序员在编写代码,或者是 用户在使用 某一个 容器等等场景,仿函数都给我们带来了很大的 方便之处。


但是,仿函数固然好用,但是同样会引来一些问题:

  • 因为 我们调用方式是,创建一个 仿函数的对象,然后在调用这个对象当中的 operator()()函数,那么调用方式就是和调用普通函数是一样的,是 类名()的方式调用。那么就会出现问题,比如上述按照 不同的成员进行比较,我们取的名字就是 类似 ComparePriceLess 这样的方式,但是如果有人取名字不按照 规范的形式去 定义,取一些 Compare1 , Compare2 ,Compare3 ········ 这些名字,那么只会苦了读代码的人。
  • 而且 ,上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。
  • 对于一些简单的,有些重复的算法支持,有点过于夸张,从上述也可以看出,对于 价格 和 评价分数这种算法一样的例子,它需要实现两个函数(operator()())函数去搞定
     

lambda表达式 语法

 看语法之前,我们先来看一个例子:
 

int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate; });
}

这个例子就是用 lambda 表达式,实现效果和 仿函数实现效果是一样的。

我们发现 lambda表达式 其实就是 匿名函数对象。

lambda 语法:
 

[capture-list] (parameters) mutable -> return-type { statement}

     lambda表达式各部分说明:

  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
     

 如:

	int x = 1, y = 2;// 相当于函数的定义auto add = [](int x, int y)->int {return x + y; };// 相当于函数的调用cout << add(x, y) << endl;return 0;

而且lambda 当中,如果明确知道了返回值类型,那么,返回值就可以不写:

	int x = 1, y = 2;// 相当于函数的定义auto add = [](int x, int y) {return x + y; };// 相当于函数的调用cout << add(x, y) << endl;return 0;

而且,在日常写 lambda表达式的时候,都是不写返回值的,因为 很多时候 lambda的返回值类型都是确定的。返回一个对象都是可以自动推导类型的,如上述的  x + y 一样。

 当然,只是返回值类型可以省略,参数列表和 其中的函数定义是不能不能省略的。

在函数定义当中,不止可以写一句 return x+y ;  同样可以向普通函数一样写多个语句:

	auto swap = [](int x, int y)->int {int tmp = x;x = y;y = tmp;};

只是需要注意的是,在最后要多一个分号,因为这本质上就是一个 语句,编辑器需要分号来识别:
 

 lambda表达式当中调用其他函数

 如果是 lambda 调用全局的函数,那么可以直接调用,没有问题:

void func()
{cout << "func()" << endl;
}int main()
{auto swap = [](int x, int y)->int {int tmp = x;x = y;y = tmp;func();};return 0;
}

 但是,如果是 

lambda表达式所在的局部当中的函数就不能直接调用,会编译报错

int main()
{auto func = []()->void {cout << "func()" << endl; };auto swap = [](int x, int y)->int {int tmp = x;x = y;y = tmp;func();};return 0;
}

报错:

lambda表达式当中的捕捉列表

 如果,你想在 lambda表达式当中使用某一个变量的值,但是又不想把这个变量传进去,那么就可以使用 lambda 表达式的 捕捉列表。

在lambda表达式最开始的 "[]" 就是捕捉列表,编译器是根据开头的 "[]" 来判断接下来的代码是否是lambda 函数的。

而捕捉列表是 捕捉上下文代码当中的变量供 lambda使用。

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

如下面这个例子:
 

	int a = 0, b = 2;double rate = 2.555;auto add1 = [](int x, int y)->int {return x + y; };auto add2 = [](int x, int y) {return x + y; };auto add3 = [rate](int x, int y) {return (x + y) * rate; };cout << add1(a, b) << endl;cout << add2(a, b) << endl;cout << add3(a, b) << endl;

输出:

2
2
5.11

捕捉列表捕捉变量的方式

  •  [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

 下面是上述 5 种捕捉变量方式的一些例子:

例1:

在 lambda表达式实现的 swap()函数当中,我想交换两个变量,但是不想以参数的方式传入这个两个变量的话,就可以传入这两个变量的引用,通过引用来修改到其中的值:

	int x = 1;int y = 2;auto swap = [x, y](){int tmp = x;x = y;y = tmp;};swap();

上述编译报错:

 error C3491: “x”: 无法在非可变 lambda 中修改通过复制捕获error C3491: “y”: 无法在非可变 lambda 中修改通过复制捕获

因为,单独捕捉 x 和 y,只是传值,是和 函数当中传入参数是一样的,因为 lambda 表达式本质上还是 函数的方式在调用,也是要创建函数栈帧的,上述 lambda 当中捕捉的 x 和 y 就和 函数当中的 形参类似,相当于是把 swap 外部的x 拷贝给 swap 当中的 x。

 而且 lambda 的捕捉列表传值是 const 的传值,只能读,不能进行修改,所说上述的代码会报错。

  如果实在想像函数传值传参的方式一样,在内部修改 函数内部的 形参的话,可以加上 mutable 关键词修饰:

	int x = 1;int y = 2;auto swap = [x, y]() mutable{int tmp = x;x = y;y = tmp;};swap();

 mutable让 捕捉到的x 和 y 可以被修改了,但是 这两个变量依旧是 外部的拷贝。在 swap()当中的 x 和 y 可以被修改了,但是不会影响到 swap()函数外部的 x 和 y,因为 是类似于 传值的方式传参,不会修改到外部变量。

但是上述的方式使用得很少,传值的方式捕捉一般只是想取到 变量的值,要想修改 外部的变量,我们一般使用的是 捕捉引用的方式捕捉变量:

	int x = 1;int y = 2;auto swap = [&x, &y](){int tmp = x;x = y;y = tmp;};swap();

注意,上述的捕捉列表当中的 &x 和 &y 不是取 x 和 y 的地址,而是表示这两个变量的引用。我们可以认为是C++11在这里的语法上的妥协。只是在 捕捉列表当中这样写 是 表示引用,其他地方还是 取地址的语法。


 我们还可以用 [&] 的方式来 以 引用捕捉 的方式 捕捉父作用域当中所有的 变量(父作用域指包含lambda函数的语句块):

int main()
{	int a = 0;int b = 1;int c = 2;int d = 3;auto swap = [&]{a = 10;b = 10;c = 10;d = 10;};swap();cout << a << " "<< b << " " << c << " " << d << " " << endl;
}

输出:
 

10 10 10 10

注意我们上述虽然传入参数,但是我们连参数列表的 "()" 都没有写,因为 ,如果没有参数的话,我们甚至连 "()" 都不用写。


 除了上述使用 "[&]" 方式,我们还可以混合着使用:

比如:
 

auto swap = [&, a]{};

这个语句的意思就是,引用捕捉的 方式 捕捉 全部的 变量,除了 a 变量之外。a变量以 传值捕捉的方式捕捉。

而且 引用捕捉的方式非常的灵活:

  •  如果捕捉的是 普通变量的引用,捕捉到的就是普通引用;
  • 如果是 const 变量,捕捉的就是 const 的引用,在函数内是不能修改的。


当然 "[=]" 这种也支持上述的混合写法。

语法上捕捉列表可由多个捕捉项组成,并以逗号分割。

比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。

捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。


lambda表达式之间不能相互赋值,即使看起来类型相同,但是会报错:
 

	auto func1 = [](int x, int y) {return  x < y; };auto func2 = [](int x, int y) {return  x < y; };func1 = func2;

编译报错:

 error C2679: 二元“=”: 没有找到接受“main::<lambda_2413383518bedbdc42de776d37947602>”类型的右操作数的运算符(或没有可接受的转换)

 我们可以打印一下上述的 两个 lambda 函数的类型:

	cout << typeid(func1).name() << endl;cout << typeid(func2).name() << endl;

输出:

class <lambda_10047583502d56c84f61e3fd5f21e4ff>
class <lambda_2413383518bedbdc42de776d37947602>

 发现其实类型是不一样的 ,类型的名是有 lambda + uuid 生成的,这个 uuid 是某个大佬做的算法,可以生成 极小概率重复的字符串。

这里就保证了,名字是基本不会冲突的。lambda的底层其实就是用仿函数实现的。 如果两个类的类名相同了,就会编译报错了。

而 上述的 func1(),func2(),两个是各自仿函数类生成的对象。对于这两个对象,对于我们是匿名的,但是编译器是知道这两个变量的 类型的。所以才需要用  auto 自动推导类型。

 lambda 的底层是仿函数,我们查看 反汇编来观察:

 我们发现,它显示调用了 lambda_uuid 这个类名的构造函数,构造出了一个对象,然后调用了这个 对象的 operator()()函数。为了防止类名冲突(重复),使用了 lambda + uuid 的方式来给这个 lambda(底层仿函数)命名。


之前,我们说 ,在 lambda 当中是不能调用 本局部域当中的函数的,其实,要想调用 局部域当中的函数,需要用 捕捉列表 捕捉这个函数,才能在 lambda 当中调用这个函数。

	auto add1 = [](int x, int y)->int {return x + y; };auto swap1 = [add1](int& x, int& y) {int tmp = x;x = y;y = tmp;cout << add1(x, y) << endl;func();};

lambda表达式和 仿函数

 从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

 

 在 C++当中就连底层实现,lambda 都是用 仿函数实现的。

但是 lambda 相对于 仿函数的类名取名时候更加严谨,因为 仿函数的名字是由写这个仿函数的人决定的,可能不会复符合规范命名。

在 lambda 当中就舍弃了 人来进行命名,在我们看来,lanmda 就是一个 匿名函数对象;但是在编译器看来,他是由 lambda + uuid 的方式组成的基本不会冲突的 名字。

但是 lambda 的使用比 仿函数更加 复杂,刚开始学的人可能对 lambda 的语法有很多疑问,但是,当熟练掌握 lambda 之后,lambda 其实是一个非常好用的语法。

相关文章:

C++ - 完美语义(右值引用的中篇) - lambda表达式

前言 之前对右值引用的理解&#xff0c;用使用场景做了详细说明&#xff0c;具体看博客&#xff1a;C - 右值引用 和 移动拷贝-CSDN博客 在 有值引用 当中还有一个 完美转发&#xff0c;请看本篇博客。 完美转发 我们现在看这个例子&#xff1a; void Fun(int& x) { …...

常见排序算法详解

目录 排序的相关概念 排序&#xff1a; 稳定性&#xff1a; 内部排序&#xff1a; 外部排序&#xff1a; 常见的排序&#xff1a; 常见排序算法的实现 插入排序&#xff1a; 基本思想&#xff1a; 直…...

监控搭建-Prometheus

监控搭建-Prometheus 1、背景2、目标3、选型4、Prometheus4.1、介绍4.2、架构4.3、构件4.4、运行机制4.5、环境介绍4.6、数据准备4.7、网络策略4.7.1、主机端口放行4.7.2、设备端口放行 4.8、部署4.9、验证4.10、配置 1、背景 随着项目信息化进程的推进&#xff0c;操作系统、…...

指纹浏览器开发指南-EasyBR

想开发一款指纹浏览器&#xff0c;指纹浏览器名字叫做EasyBR&#xff0c;大致构思了下开发的步骤。 EasyBR指纹浏览器开发指南&#xff1a; 后台技术、前端技术和指纹修改 简介&#xff1a; EasyBR指纹浏览器是一款旨在提供个性化服务和广告定位的浏览器&#xff0c;通过收…...

qml入门

window import QtQuick 2.15 import QtQuick.Window 2.15 import QtQuick.Controls 2.5Window { //root控件&#xff0c;父窗口是主界面width: 640height: 480visible: true//相对于父控件的偏移量x: 100y:100minimumWidth: 400 //最小宽度minimumHeight: 300 //最小高度ma…...

一文熟练使用python修改Excel中的数据

使用python修改Excel中的内容 1.初级修改 1.1 openpyxl库的功能&#xff1a; openpyxl模块是一个读写Excel 2010文档的Python库&#xff0c;如果要处理更早格式的Excel文档&#xff0c;需要用到额外的库&#xff0c;例如Xlwings。openpyxl是一个比较综合的工具&#xff0c;能…...

java Spring Boot在配置文件中关闭热部署

之前更大家一起搭建了一个热部署的开发环境 但是 大家要清楚一个情况 我们线上程序运行突然内部发生变化这是不可能的。 所以 他就只会对我们开发环境有效 是否开启 我们可以通过 application配置文件来完成 我这里是yml格式的 参考代码如下 spring:devtools:restart:enabled…...

【物联网】Arduino+ESP8266物联网开发(一):开发环境搭建 安装Arduino和驱动

ESP8266物联网开发 1.开发环境安装 开发软件下载地址&#xff1a; 链接: https://pan.baidu.com/s/1BaOY7kWTvh4Obobj64OHyA?pwd3qv8 提取码: 3qv8 1.1 安装驱动 将ESP8266连接到电脑上&#xff0c;安装ESP8266驱动CP210x 安装成功后&#xff0c;打开设备管理器&#xff0c…...

自定义UI对象转流程节点

自定义UI对象转流程节点 实体自定义对象转bpmn activitiy学习 (动态加签&#xff0c;动态流程图&#xff0c;指定节点跳转&#xff0c;指定多人节点跳转) 前端页面仿的这个 提供一个思路 实体 ActivitiValueVo import io.swagger.annotations.ApiModel; import io.swagger.a…...

P1-P5_动手学深度学习-pytorch(李沐版,粗浅的笔记)

目录 预告  1.学习深度学习的关键是动手  2.什么是《动手学深度学习》  3.曾经推出的版本&#xff08;含github链接&#xff09; 一、课程安排  1.目标  2.内容  3.上课形式  4.你将学到什么  5.资源 二、深度学习的介绍  1.AI地图  2.深度学习在一些应用上…...

Android Studio修改模拟器AVD Manger目录

Android Studio修改虚拟机AVD Manger目录 1、在AS的设备管理器Device Manager中删除原来创建的所有虚拟机&#xff08;Android Virtual Device&#xff09;&#xff1b; 2、新建一个自定义的AVD目录&#xff0c;例如&#xff1a;D:\Android\AndroidAVD 3、在高级系统设置中增加…...

STM32--MQ2烟雾传感器

本文主要介绍STM32F103C8T6和烟雾传感器模块的控制算法 简介 烟雾模块选用MQ-2气体传感器&#xff0c;根据传感器的电导率随空气中可燃气体浓度的增加而增大的特性检测空气中可燃气体&#xff0c;然后将电导率的变化转换成对应的电信号 MQ系列烟雾传感分类如下&#xff1a; 该…...

GitHub要求开启2FA,否则不让用了。

背景 其实大概在一个多月前&#xff0c;在 GitHub 网页端以及邮箱里都被提示&#xff1a;要求开启 2FA &#xff0c;即双因子认证&#xff1b;但是当时由于拖延症和侥幸心理作祟&#xff0c;直接忽略了相关信息&#xff0c;毕竟“又不是不能用”。。 只到今天发现 GitHub 直接…...

Python 编程基础 | 第三章-数据类型 | 3.6、元组

一、元组 Python 的元组与列表类似&#xff0c;不同之处在于元组的元素不能修改。元组使用小括号&#xff0c;列表使用方括号。 1、创建元组 元组创建很简单&#xff0c;只需要在括号中添加元素&#xff0c;并使用逗号隔开即可&#xff0c;例如&#xff1a; tup1 (physics, ch…...

2023/10/7 -- ARM

【程序状态寄存器读写指令】 1.指令码以及格式 mrs:读取CPSR寄存器的值 mrs 目标寄存器 CPSR&#xff1a;读取CPSR的数值保存到目标寄存器中msr:修改CPSR寄存器的数值msr CPSR,第一操作数:将第一操作数的数值保存到CPSR寄存器中//修改CPSR寄存器&#xff0c;也就表示程序的状…...

yolov5加关键点回归

文章目录 一、数据1&#xff09;数据准备2&#xff09;标注文件说明 二、基于yolov5-face 修改自己的yolov5加关键点回归1、dataloader,py2、augmentations.py3、loss.py4、yolo.py 一、数据 1&#xff09;数据准备 1、手动创建文件夹: yolov5-face-master/data/widerface/tr…...

untitle

实用的科研图形美化处理教程分享 ​ 显微照片排版标记 除了统计图表之外&#xff0c;显微照片也是文章中必不可少的实验结果呈现方式。除了常规实验的各种组织切片照片&#xff0c;在空间转录组文章中显微照片更是常见。显微照片的呈现方式也是有讲究的&#xff0c;比如对照片…...

《论文阅读》监督对抗性对比学习在对话中的情绪识别 ACL2023

《论文阅读》监督对抗性对比学习在对话中的情绪识别 前言摘要相关知识最坏样本干扰监督对比学习生成式对抗网络纳什均衡琴森香农散度范式球模型架构监督对抗性对比学习模型结构图实验结果问题前言 你是否也对于理解论文存在困惑? 你是否也像我之前搜索论文解读,得到只是中文…...

2023-10-07 LeetCode每日一题(股票价格跨度)

2023-10-07每日一题 一、题目编号 901. 股票价格跨度二、题目链接 点击跳转到题目位置 三、题目描述 设计一个算法收集某些股票的每日报价&#xff0c;并返回该股票当日价格的 跨度 。 当日股票价格的 跨度 被定义为股票价格小于或等于今天价格的最大连续日数&#xff08…...

聊聊分布式架构04——RPC通信原理

目录 RPC通信的基本原理 RPC结构 手撸简陋版RPC 知识点梳理 1.Socket套接字通信机制 2.通信过程的序列化与反序列化 3.动态代理 4.反射 思维流程梳理 码起来 服务端时序图 服务端—Api与Provider模块 客户端时序图 RPC通信的基本原理 RPC&#xff08;Remote Proc…...

维吉尼亚密码

维吉尼亚密码属于多表代换密码 其中A<–>0&#xff0c;B<–>1&#xff0c;…&#xff0c;Z<–>25&#xff0c;则每个密钥K相当于一个长度为m的字母串&#xff0c;称为密钥字。维吉尼亚密码一次加密m个明文字母。 示例&#xff1a;设m6&#xff0c;密钥字为…...

ubuntu20.04挂载拓展盘保姆级流程

背景&#xff1a;跑模型玩时&#xff0c;发现机子硬盘太小了&#xff0c;搞个1t固态作为挂载盘。以下为操作全流程记录 1、开始root权限操作 sudo su若进不去&#xff0c;考虑是否给root设置过密码&#xff0c;新系统第一次进入需要设置密码。 进入成功&#xff1a; rooty:…...

顶顶通电话机器人接口对接开源ASR(语音识别)

前景介绍 目前大部分用户使用的都是在线ASR按照分钟或者按次付费&#xff0c;之前开源ASR效果太差不具备商用的条件&#xff0c;随着 阿里达摩院发布了大量开源数据集或者海量工业数据训练的模型&#xff0c;识别效果已经和商用ASR差距非常小&#xff0c;完全具备了很多场景代…...

windows消息机制

windows开发比较简单&#xff0c;首先要理解的就是消息机制。 Windows消息机制是指Windows操作系统中的消息传递机制。在Windows中&#xff0c;应用程序通过消息进行通信和交互。消息是一种轻量级的通信方式&#xff0c;用于在不同的窗口、线程或进程之间传递信息。 在Windows…...

整数划分——DP

用 j j j 个数表示 i i i 的方案数&#xff0c;考虑dp 转移考虑最小值是否为1 无限制 若为1&#xff0c;则转移到 f ( i 1 , j 1 ) f(i1, j1) f(i1,j1)不为1&#xff0c;则全部1&#xff0c;转移到 f ( i j , j ) f(ij, j) f(ij,j) 数之间不能重复 那么相当于每次整…...

Git切换用户常用命令

1、查看 查看用户名 &#xff1a; git config user.name查看密码&#xff1a; git config user.password查看邮箱&#xff1a; git config user.email查看配置信息&#xff08;包含上面的信息&#xff09;&#xff1a; $ git config --list2、新增、切换 修改用户名 git…...

一般香港服务器带宽选多大够用?(带宽计算方法)

​  在海外IDC市场份额中&#xff0c;香港服务器依托自身优越的服务器资源条件&#xff0c;在各个行业中发挥的重要作用。但是&#xff0c;不同业务对网络带宽的要求各不相同&#xff0c;弄清楚如何计算带宽需求对于确保业务平稳运行至关重要&#xff0c;最好从一开始就使用正…...

vue中使用ali-oss上传文件到阿里云上

1.使用 npm 安装ali-oss npm install ali-oss --save2.写ali-oss.js // 引入ali-oss let OSS require(ali-oss) let client new OSS({region: oss-cn-xxx, // bucket所在的区域&#xff0c; 默认oss-cn-hangzhousecure: true, // secure: 配合region使用&#xff0c;如果指…...

php实战案例记录(17)计算时间的函数及其示例说明

在PHP中&#xff0c;有许多函数可以用于计算和处理时间。以下是一些常用的计算时间的函数及其示例说明&#xff1a; time()&#xff1a;获取当前时间的Unix时间戳。 $currentTimestamp time(); echo $currentTimestamp;date()&#xff1a;将Unix时间戳格式化为指定的日期和时…...

基于Keil a51汇编 —— MPL 宏定义

MPL 宏 Ax51汇编程序支持的宏处理语言&#xff08;MPL&#xff09;是一种字符串替换工具&#xff0c;使您能够编写可修复的代码块&#xff08;宏&#xff09;并将其插入源文本中的一个或多个位置。 宏处理器查看源文件的方式与汇编程序不同。 对于汇编程序来说&#xff0c;源…...