本篇博客来自于深蓝学院c++课程,自己整理总结得到。
可调用对象
- 函数指针:概念直观,但定义位置受限,因为c++不支持函数中定义函数
- 类:功能强大,但是写起来麻烦
- bind:基于已有的逻辑灵活适配,但在描述复杂的逻辑时语法比较复杂难懂
- lambda表达式:小巧灵活,功能强大
bind方法使用
bind:通过绑定的方式修改可调用对象的调用方式
- std::bind ( C++11 引入):用于修改可调用对象的调用方式
– 调用 std::bind 时,传入的参数会被复制,这可能会产生一些调用风险
– 可以使用 std::ref 或 std::cref 避免复制的行为
案例1:bind基本使用
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
bool MyPredict(int val1, int val2)
{
return val1 > val2;
}
int main(int argc, char**argv)
{
using namespace std::placeholders;
std::vector<int> x{1,2,3,4,5,6,7,8,9};
std::vector<int> y;
std::copy_if(x.begin(), x.end(), std::back_inserter(y), std::bind(MyPredict, _1 , 3));
for (auto value : y)
{
std::cout <<value << " ";
}
std::cout << std::endl;
// 结果为:4 5 6 7 8 9
}
深度解析bind参数含义
这里,单独来探究bind中传入参数的意义。仍然以案例1来进行分析,首先看下面这段程序:
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
bool MyPredict(int val1, int val2)
{
return val1 > val2;
}
int main(int argc, char**argv)
{
using namespace std::placeholders;
auto x = std::bind(MyPredict, _1, 3);
std::cout << x(50) << std::endl; // 1
auto x = std::bind(MyPredict, 3, _1);
std::cout << x(50) << std::endl; // 0
}
bind中的_1(std::placeholders::_1)是绑定的传入x(50)的第一个参数,并不是MyPredict中的val1。std::bind(MyPredict, 3, _1)中3对应的是MyPredict的va1,_1对应MyPredict的val2,也就是说bind中参数的顺序对应绑定的可调用对象的参数顺序,但是std::placeholders::_1绑定的是传入x的第一个参数,这点非常重要。
为了进一步理解,看下面这段代码:
bool MyPredict(int val1, int val2)
{
return val1 > val2;
}
int main(int argc, char**argv)
{
using namespace std::placeholders;
auto x = std::bind(MyPredict, _2, 3);
std::cout << x(1, 50) << std::endl; // 1
}
这里的返回结果仍然为1,原因是,_2绑定的传入x的第二个参数50。
进一步,我们可以看到:
bool MyPredict(int val1, int val2)
{
return val1 > val2;
}
int main(int argc, char**argv)
{
using namespace std::placeholders;
auto x = std::bind(MyPredict, _2, _1);
std::cout << x(3, 4) << std::endl; // 1
}
这里返回1的原因,和上面原理一模一样,_2即传入x(3,4)的第二个参数4.与此同时_2绑定的是MyPredict的val1。同理分析val2。
案例2:bind进阶用法
bool MyPredict(int val1, int val2)
{
return val1 > val2;
}
bool MyAnd(bool val1, bool val2)
{
return val1 && val2;
}
int main(int argc, char**argv)
{
using namespace std::placeholders;
auto x1 = std::bind(MyPredict, _1, 3);
auto x2 = std::bind(MyPredict, 10, _1);
auto x3 = std::bind(MyAnd, x1, x2);
std::cout << x3(5); // 1
}
结果分析:先会处理x1,在处理x2,x1和x2返回的值在带入到MyAnd。实则这个程序在判断给的结果是否在3和10之间。
bind的注意事项
1.调用 std::bind 时,传入的参数会被复制,这可能会产生一些调用风险
bool MyPredict(int val1, int val2)
{
return val1 > val2;
}
bool MyAnd(bool val1, bool val2)
{
return val1 && val2;
}
void MyProc(int* ptr) {}
auto fun()
{
int x;
return std::bind(MyProc, &x);
}
int main(int argc, char**argv)
{
auto ptr = fun();
ptr(); // 行为未定义
}
结果分析:这个代码存在风险,ptr()的行为未定义。具体原因为:std::bind(MyProc, &x)返回一个可调用对象,但是&x指向的是一个局部变量并且x地址会被复制到bind对象中,当fun()函数结束时,局部变量已经为销毁了。
解决方法:智能指针
void MyProc(std::shared_ptr<int> ptr) {}
auto fun()
{
std::shared_ptr<int> x(new int());
return std::bind(MyProc, x);
}
int main(int argc, char**argv)
{
auto ptr = fun();
}
2.使用 std::ref 或 std::cref 避免复制的行为
先看一段代码:
void Proc(int& x)
{
++x;
}
int main(int argc, char**argv)
{
int x = 0;
auto b = std::bind(Proc, x);
b();
std::cout << x << std::endl; // 0
}
结果分析:我们想得到的应该是1,但是结果为0。原因就是x是被拷贝复制到b中,可以理解为b中的x被修改,但是原本的x并没有发生变化。
解决方法: std::ref 或 std::cref
void Proc(int& x)
{
++x;
}
int main(int argc, char**argv)
{
int x = 0;
auto b = std::bind(Proc, std::ref(x));
b();
std::cout << x << std::endl; // 1
}
lambda表达式
-
lambda表达式的基本组成部分
–参数与函数体
–返回类型
–捕获:针对函数体中使用的局部自动对象进行捕获- 值捕获、引用捕获与混合捕获
- this捕获
- 初始化捕获(c++14)
- *this捕获(c++17)
–说明符
- mutable/constexpr(c++17)
-
lambda表达式的深度应用
– 捕获时计算(c++ 14)
– 即调用函数表达式
– 使用auto避免赋值(c++ 14)
– Lifting(c++ 14)
– 递归调用(c++14)
案例1:lambda表达式的基本使用:参数与函数体
#include <iostream>
int main(int argc, char**argv)
{
auto x = [](int val){return (val > 3) && (val < 10);};
std::cout << x(5) << std::endl; // 1
}
案例2:lambda表达式的基本使用:返回类型
c++11开始可以自动推导lambda表达式的返回类型,但是要保证lambda表达式内部的返回值一致,这样才可以自动推导,如果不一致,就必须显示指定返回类型。来看一个例子:
int main(int argc, char**argv)
{
auto x = [](int val) -> float
{
if (val > 5) return 3.0;
else return 1.5f;
};
}
如果这里不显示指定返回类型为float,程序会报错。因为3.0是double类型,1.5f是float类型,编译器无法完成自动类型推导。
案例3:lambda表达式的捕获(非常重要)
1. 局部自动对象进行捕获
先来看一个报错的例子,如下:
int main(int argc, char**argv)
{
int y = 10;
auto x = [](int val) {return val > y;};
}
错误为:
error: ‘y’ is not captured
分析一下原因:因为lambda表达式里面用到了y,但是y是一个局部变量并且在lambda表达式函数体外部,lambda表达式是无法访问的,所以报错。那怎么解决呢?其实很简单,捕获y即可,具体操作如下:
int main(int argc, char**argv)
{
int y = 10;
auto x = [y](int val) {return val > y;};
std::cout << x(3) << std::endl; // 0
}
下面,继续讨论一下,我们要知道捕获的是局部自动对象,那如果y是全局变量或者静态变量,可不可以不捕获,直接在lambda表达式内部进行使用呢?答案是可以。因此,针对报错代码,还可以这么进行修改。
int main(int argc, char**argv)
{
static int y = 10;
auto x = [](int val) {return val > y;};
std::cout << x(3) << std::endl; // 0
}
int y = 10;
int main(int argc, char**argv)
{
auto x = [](int val) {return val > y;};
std::cout << x(3) << std::endl; // 0
}
所以,一定要明白,lambda表达式捕获的是局部自动对象。
2. 值捕获
int main(int argc, char **argv) {
int y = 10;
auto x = [y](int val) mutable
{
++y;
return val > y;
};
std::cout << x(3) << std::endl; // 0
std::cout << y << std::endl; // 10
}
分析一下:先不要管说明符mutable,后续讨论。这个程序的运行逻辑是:线对y执行加1操作,然后再将val于y进行比较。但是我们可以看到y的返回值仍然是10,不是11。这是为啥呢?原因是:值捕获是将捕获的对象复制到lambda表示式内部(或者说类内部,因为lambda表达式的本质是类,编译器是将lambda表达式翻译成类的形式,这也是lambda表达式功能强大的重要原因),那我们知道lambda表达式内部执行的++y,实则只是对复制到类内部的y进行加1操作,对外部定义的y没有进行任何操作,因此返回值仍然是10。如果大家不相信,觉得这个很神奇,可以借用**c++ insights**进行查看,这里我也展示一下吧。
int main(int argc, char ** argv)
{
int y = 10;
class __lambda_4_14
{
public:
inline /*constexpr */ bool operator()(int val)
{
++y;
return val > y;
}
private:
int y;
public:
__lambda_4_14(int & _y)
: y{_y}
{}
};
__lambda_4_14 x = __lambda_4_14{y};
return 0;
}
这段代码其实就是c++内部对lambda进行的处理,很显然可以看出来在类的内部定义了一个成员变量为y,然后捕获的y是复制给成员变量y,那么y本身肯定是没有发生任何改变的。
3. 引用捕获
与值捕获恰恰相反的是引用捕获,我们来看看上面那个例子:
int main(int argc, char **argv) {
int y = 10;
auto x = [&y](int val)
{
++y;
return val > y;
};
std::cout << x(3) << std::endl; // 0
std::cout << y << std::endl; // 11
}
分析一下,这里返回的值就是11了,不在是10了。这是为啥呢,在此借用c++ insights看一下就一目了然了哈。
int main(int argc, char ** argv)
{
int y = 10;
class __lambda_4_14
{
public:
inline /*constexpr */ bool operator()(int val) const
{
++y;
return val > y;
}
private:
int & y;
public:
__lambda_4_14(int & _y)
: y{_y}
{}
};
__lambda_4_14 x = __lambda_4_14{y};
return 0;
}
大家看出来区别了吗?与值捕获不同的是,类内部是int&,那么在把y传给类内部的成员变量时,相当于是外部y的一个别名,那么在进行++操作时,对外部的y也进行了++操作,因此返回11。这下就一目了然了。
4. 混合捕获
先看一个简单的例子,知道啥叫混合捕获
int main(int argc, char **argv) {
int y = 10;
int z = 3;
auto x = [&y, z](int val) mutable
{
++y;
++z;
return val > z;
};
std::cout << x(3) << std::endl; // 0
std::cout << y << std::endl; // 11
std::cout << z << std::endl; // 3
}
注意观察捕获列表,对y是引用捕获,返回值为11,对z是值捕获,返回值为3。这就是混合捕获了。
有的时候,我们可能需要捕获很多局部自动对象,这时候在一个个写太麻烦了,那么这是怎么办呢?
int main(int argc, char **argv) {
int y = 10;
int w = 10;
int z = 3;
auto x = [&, z](int val) mutable
{
++y;
++z;
++w;
return val > z;
};
std::cout << x(3) << std::endl; // 0
std::cout << y << std::endl; // 11
std::cout << z << std::endl; // 3
std::cout << w << std::endl; // 11
}
在观察捕获列表[&, z],代表的含义为,除了z为值捕获,其他均为引用捕获。那么,大家可以联想很多了,假如是[&]就代表所有对象都是引用捕获,[=, &z]代表除了z是引用捕获,其他都是值捕获。以后遇到了其他的,以此类推即可。
5. this捕获
struct Str
{
auto fun()
{
int val = 3;
auto lam = [val, this]()
{
return val > x;
};
return lam();
}
int x;
};
int main(int argc, char **argv)
{
Str s;
s.fun();
}
分析一下,因为结构体中定义的x不是局部自动对象(因为没有定义在函数fun()内部),因此之前介绍的捕获方法都无法使用,这时候就需要使用this捕获,this其实是一个指针,指向Str这个地址,当我们调用fun()函数时,this指向的Str地址中有变量x,此时就可以捕获了。
6. 初始化捕获(c++14)
优点:(1)可以引入复杂的捕获逻辑;(2)一定程度提升系统性能
简单使用:
int main(int argc, char **argv)
{
int x = 3;
auto lam = [y = x](int val)
{
return val > y;
};
std::cout << lam(100) << std::endl; // 1
}
捕获列表中的=不是值捕获,具体含义为:先构造一个局部自动对象y,然后将x赋给y,然后val于y在进行比较。
优点1:复杂捕获逻辑
int main(int argc, char **argv)
{
std::string a = "hell0";
auto lam = [y = std::move(a)]()
{
std::cout << y << std::endl;
};
std::cout << a << std::endl; // 空
lam(); // hello
}
注意一下,这里的a为空是发生在lam()调用之前。
优点2:一定程度提升系统性能
int main(int argc, char **argv) {
int x = 10;
int y = 20;
auto lam = [z = x + y](int val) {
return val > z;
};
lam(3);
}
这段代码的目的是计算x+y的值然后与传入的val进行比较,如果使用初始化捕获,x+y的值会被保存在z中,每次调用只会将z与val进行比较。但是,如果使用之前的方式,代码如下:
int main(int argc, char **argv) {
int x = 10;
int y = 20;
auto lam = [x, y](int val) {
return val > (x + y);
};
lam(3);
}
此时,程序执行的逻辑是,每次调用都需要计算x+y然后再进行比较。很显然,初始化捕获可以提升系统的性能。
7. *this捕获
struct Str {
auto fun() {
int val = 3;
auto lam = [val, *this]() {
return val > x;
};
return lam;
}
int x;
};
auto wrapper()
{
Str s;
return s.fun();
}
int main(int argc, char **argv) {
auto lam = wrapper();
lam();
}
这段代码与捕获this非常相似,但是本质完全不同。*this是解引用,是Str对象,this是指向Str的对象,*this是通过复制Str对象到lambda表达式内部,但是这样也会带来问题,复制会占用资源。如果这里用this,那么在调用wrapper()时,lam是一个悬挂的指针,指向了已经销毁的对象,这是因为如果使用this,那么this指向的是Str s,这是一个局部自动对象,在调用wrapper()之后就已经被销毁了。
说明符
1. mutable
可以对类内部的成员进行修改
int main(int argc, char **argv) {
int y = 10;
auto lam = [y](int val) mutable
{
++y;
return val > y;
};
}
我们在c++ insights中看一下:
int main(int argc, char ** argv)
{
int y = 10;
class __lambda_3_16
{
public:
inline /*constexpr */ bool operator()(int val)
{
++y;
return val > y;
}
private:
int y;
public:
__lambda_3_16(int & _y)
: y{_y}
{}
};
__lambda_3_16 lam = __lambda_3_16{y};
return 0;
}
重载()运算符后面没有const,但是如果没有mutable说明符,在观察一下:
int main(int argc, char ** argv)
{
int y = 10;
class __lambda_3_16
{
public:
inline /*constexpr */ bool operator()(int val) const
{
return val > y;
}
private:
int y;
public:
__lambda_3_16(int & _y)
: y{_y}
{}
};
__lambda_3_16 lam = __lambda_3_16{y};
return 0;
}
可以明显看到,bool operator()(int val) const 有一个const,说明无法对类内部的数据进行改变,因为也就无法实现++y的功能。
2. constexpr c++17
好处:可以在编译期进行调用
int main(int argc, char **argv) {
auto lam = [](int val) constexpr
{
return val + 1;
};
constexpr int val = lam(100);
std::cout << val << std::endl;
}
lambda表达式的深入应用
1. 捕获时计算 c++14
int main(int argc, char **argv) {
int x = 3, y = 5;
auto lam = [z = x + y]()
{
return z;
};
std::cout << lam() << '\n'; // 8
}
2. 即调用函数表达式(IIFE)
优势:可以初始化一些常量
int main(int argc, char **argv) {
int x = 3, y = 5;
const auto val = [z = x + y]()
{
return z;
}();
}
3. 使用auto避免复制 c++14
#include <iostream>
#include <map>
int main(int argc, char **argv) {
std::map<int, int> m{{2,3}};
auto lam = [](const auto& p)
{
return p.first + p.second;
};
std::cout << lam(*m.begin()) << '\n'; // 5
}
如果这里不得auto,写成下面代码:
int main(int argc, char **argv) {
std::map<int, int> m{{2, 3}};
auto lam = [](const std::pair<int, int> &p) {
return p.first + p.second;
};
std::cout << lam(*m.begin()) << '\n'; // 5
}
此时在调用的时候会产生多余的复制操作,程序将*m.begin()复制给p。
4. Lifting c++14
auto fun(int val)
{
return val + 1;
}
// 函数重载
auto fun(double val)
{
return val + 1;
}
int main(int argc, char **argv) {
auto lam = [](auto x)
{
return fun(x);
};
std::cout <<lam(3) << '\n'; // 4
std::cout <<lam(3.5) << '\n'; // 4.5
}
5. 递归调用 c++14
int main(int argc, char **argv) {
auto factorial = [](int n) {
auto f_impl = [](int n, const auto &impl) -> int {
return n > 1 ? n * impl(n - 1, impl) : 1;
};
return f_impl(n, f_impl);
};
std::cout << factorial(5) << std::endl; // 120
}