北京高端网站定制公司,建设银行 网站设置密码,网站如何paypal支付方式,在大网站做网页广告需要多少钱前言
之前对右值引用的理解#xff0c;用使用场景做了详细说明#xff0c;具体看博客#xff1a;C - 右值引用 和 移动拷贝-CSDN博客
在 有值引用 当中还有一个 完美转发#xff0c;请看本篇博客。
完美转发 我们现在看这个例子#xff1a;
void Fun(int x) { …前言
之前对右值引用的理解用使用场景做了详细说明具体看博客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; }
// 模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
// 但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
templatetypename 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; }templatetypename 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 都是左值movea这个表达式是 右值右值引用的 功能是右值引用但是属性不是右值而是左值因为 右值引用支持修改。而且理论上右值引用必须支持修改之前的移动拷贝就是要修改 有值引用当中的数据和 另一个对象当中的数据进行交换。 比如在 string 当中使用的移动拷贝 我们使用 swap 函数来进行交换两个string对象当中的数据但是 swap函数的参数是 string 是一个 非const 的左值引用是不能接受右值的如果 string拷贝构造函数当中的 s 有值引用的属性是左值的话 swap怎么可能接受呢 所以看懂上述这个小例子你应该就清楚为什么上述 PerfectForward函数不管传入左值还是有值调用的都是 左值版本的func了因为右值引用的 功能是右值引用但是属性不是右值而是左值 完美转发的使用 那么如上述例子我们在模版当中模版类型 的 是万能引用但是不管是 左值引用还是 右值引用他们的属性都是左值也就是说如果在模版函数或者说 模版类当中的成员函数当中使用模版类型的形参 调用某个函数的话只能是左值引用参数类型的函数。
但是右值引用的优化在很多地方都会用到在模版函数类模版的成员函数当中也是非常多用的。所以这时候就要使用完美转发了。 完美转发的使用方式 跟 move 类似使用 forward模版参数模版类型的方式来使用如上述例子应该这样修改
templatetypename T
void PerfectForward(T t)
{// 完美转发Fun(forwardT(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; }templatetypename T
void PerfectForward(T t)
{Fun(forwardT(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 右值引用
我们发现结果就符合我们的预期了。 在类模版当中比如写了一个函数的 左值引用版本 和 右值引用版本但是又想只用一个的话不能直接把 某一个删掉
templateclass 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结点结构体当中的构造函数当中使用的模版参数是 类模板的模版参数不是推导的是我们在定义 listT 的时候传入的参数已经定好了。
如果实在想要像上述一样只写一个的话就要在类模板当中加一个这个函数的函数模版 完美转发的意义 我们上述说明了 右值引用的两种使用场景 首先是 移动语义也就是移动构造和 移动拷贝在一定语义当中因为需要对某一些空间的替换移动所以右值引用的对象要可以修改所以此时右值引用的属性是左值的我们可以取地址对其中的 成员进行修改所以这也是 右值引用的属性是 左值的一大原因。
但是在上述我们说的例子当中在模版函数和 类模版的成员函数当中是用 模版类型的形参调用某一函数的时候如果不使用移动语义就只能是 左值引用版本的函数这肯定不是我们期望的。
右值引用带来了这么大的性能提升我们为什么不用呢
那么此时我们期望右值引用的属性是左值但是不能直接替换因为在上述的移动拷贝当中要使用 左值的属性。
所以这时候就有了完美转发的出现了。 如果像是移动拷贝这种 右值引用 一定要是 左值属性的那么就直接使用右值引用即可如果是想要继承 原本 左/右值引用的 对象的 属性是左值引用属性就是左值是右值引用属性就是右值那么就使用完美转发的继承 左/右值引用 原本的属性即可。 而移动构造移动拷贝并不是延长了某一个变量的生命周期而是把 上一个变量的空间转移到另一个 变量当中去了你可以理解为 这个空间的生命周期延长了但是上一个变量的声明周期没有延长但是单独的空间是没有声明周期这个概念的这个概念是在变量当中的。 在C11 之后每一个容器当中的 insert和 push_back函数都进行了 对 右值引用版本的实现升级。那么在 push_back函数当中我们一般是 复用 insert函数就够用了的但是如果在 右值引用版本的 push_back复用 insert函数的话我们肯定是期望复用 右值引用版本的 insert函数那么我们不能直接使用 push_back传入的 右值引用形参来调用 insert函数了因为 右值引用默认是 左值属性所以这时候我们就要使用 forward 完美转发来强行把 这里的 右值引用函数的属性转化为 左值的属性。
而且在一个类当中这个右值引用可能会多次传比如上述 从 push_back函数传入到 insert当中可能要经历多个函数的传递为了保证传递之后还是右值的属性在每一次传的时候都要进行 完美转发 传的每一次都需要 完美转发才能继承上一个 属性。 lambda表达式
lambda 表达式的出身和仿函数的比较 lambda表达式可以说是用来替代 函数指针的还可以替代一些仿函数的功能。
如果我们想要对 某一种比较方法进行替换的话可以使用仿函数的方式来实现。
像是在 sort函数的当中就使用仿函数来实现目标数据排升序还是降序
#include algorithm
#include functionalint 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]), greaterint());return 0;
} 如上述所示在库当中有两个仿函数一个是less一个是 greater因为仿函数是用一个类当中的 operator运算符重载来解决普通operator同参数不能重载的问题。
像上述一样我们就可以传入 less 或者 是 greater 类的 匿名对象来控制 sort函数是排升序还是排降序了。
而仿函数除了实现 比较顺序的选择还可以自定义比较规则比如在一个类当中有 名字价格 评价分数。那么我们可以先三个仿函数分别可以按照 名字的 string类进行比较还可以按照 价格的大小或者评价分数的大小来进行比较。
比如我们自己来实现下述类的排序
#include functional
#include algorithmstruct 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()
{vectorGoods 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()
{vectorGoods 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 xy ; 同样可以向普通函数一样写多个语句 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 的地址而是表示这两个变量的引用。我们可以认为是C11在这里的语法上的妥协。只是在 捕捉列表当中这样写 是 表示引用其他地方还是 取地址的语法。 我们还可以用 [] 的方式来 以 引用捕捉 的方式 捕捉父作用域当中所有的 变量(父作用域指包含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的底层其实就是用仿函数实现的。 如果两个类的类名相同了就会编译报错了。
而 上述的 func1func2两个是各自仿函数类生成的对象。对于这两个对象对于我们是匿名的但是编译器是知道这两个变量的 类型的。所以才需要用 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 其实是一个非常好用的语法。