闭包,函数对象包装器,Lambda 表达式
1 什么是闭包
闭包有很多种定义,一种说法是,闭包是带有上下文的函数。说白了,就是有状态的函数。更直接一些,不就是个类吗?换了个名字而已。
一个函数,带上了一个状态,就变成了闭包了。那什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量,这些个变量的值是创建闭包的时候设置的,并在调用闭包的时候,可以访问这些变量。
函数是代码,状态是一组变量,将代码和一组变量捆绑 (bind) ,就形成了闭包。
闭包的状态捆绑,必须发生在运行时。
2 闭包的实现
2.1 仿函数:重载 operator()
class MyFunctor
{
public:
MyFunctor(int tmp) : round(tmp) {}
int operator()(int tmp) { return tmp + round; }
private:
int round;
};
int main()
{
int round = 2;
MyFunctor f(round);//调用构造函数
cout << "result = " << f(1) << endl; //operator()(int tmp)
return 0;
}
2.2 std::bind绑定器
2.2.1 std::function
在C++中,可调用实体主要包括:函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了opetator()的对象。
C++11中,新增加了一个std::function类模板,它是对C++中现有的可调用实体的一种类型安全的包裹。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。std::function 是一种通用、多态的函数封装,它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作,它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数
指针的调用不是类型安全的),换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便的将函数、函数指针作为对象进行处理
#include <iostream>
#include <functional> //std::function
using namespace std;
void func(void)
{//普通全局函数
cout << __func__ << endl;
}
class Foo
{
public:
static int foo_func(int a)
{//类中静态函数
cout << __func__ << "(" << a << ") ->: ";
return a;
}
};
class Bar
{
public:
int operator()(int a)
{//仿函数
cout << __func__ << "(" << a << ") ->: ";
return a;
}
};
int main()
{
//绑定一个普通函数
function< void(void) > f1 = func;
f1();
//绑定类中的静态函数
function< int(int) > f2 = Foo::foo_func;
cout << f2(111) << endl;
//绑定一个仿函数
Bar obj;
f2 = obj;
cout << f2(222) << endl;
/*
运行结果:
func
foo_func(111) ->: 111
operator()(222) ->: 222
*/
return 0;
}
std::function对象最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等,但是可以与NULL或者nullptr进行比较。
2.2.2 std::bind
std::bind是这样一种机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。
C++98中,有两个函数bind1st和bind2nd,它们分别可以用来绑定functor的第一个和第二个参数,它们都是只可以绑定一个参数,各种限制,使得bind1st和bind2nd的可用性大大降低。
在C++11中,提供了std::bind,它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定,这个bind才是真正意义上的绑定。std::bind用来绑定函数调用的参数,它解决的需求是我们有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过这个函数,我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用
std::bind的基本语法:
#include <iostream>
#include <functional> //std::bind
using namespace std;
void func(int x, int y)
{
cout << x << " " << y << endl;
}
int main()
{
bind(func, 1, 2)(); //输出:1 2
bind(func, std::placeholders::_1, 2)(1);//输出:1 2
using namespace std::placeholders; // adds visibility of _1, _2, _3,...
bind(func, 2, _1)(1); //输出:2 1
bind(func, 2, _2)(1, 2); //输出:2 2
bind(func, _1, _2)(1, 2); //输出:1 2
bind(func,_2, _1)(1, 2); //输出:2 1
//err, 调用时没有第二个参数
//bind(func, 2, _2)(1);
return 0;
}
std::placeholders::_1是一个占位符,代表这个位置将在函数调用时,被传入的第一个参数所替代。
2.2.3 std::bind和std::function配合使用
#include <iostream>
#include <functional> //std::cout
using namespace std;
using namespace std::placeholders; // adds visibility of _1, _2, _3,...
class Test
{
public:
int i = 0;
void func(int x, int y)
{
cout << x << " " << y << endl;
}
};
int main()
{
Test obj; //创建对象
function<void(int, int)> f1 = bind(&Test::func, &obj, _1, _2);
f1(1, 2); //输出:1 2
function< int &()> f2 = bind(&Test::i, &obj);
f2() = 123;
cout << obj.i << endl;//结果为 123
return 0;
}
通过std::bind和std::function配合使用,所有的可调用对象均有了统一的操作方法。
2.3 lambda表达式
2.3.1 lambda基础使用
lambda 表达式(lambda expression)是一个匿名函数,lambda表达式基于数学中的 λ 演算得名。
C++11中的lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。
lambda表达式的基本构成:
![img](file:///C:/Users/Dark/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png)
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
① 函数对象参数
[],标识一个lambda****的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义lambda为止时lambda所在作用范围内可见的局部变量(包括lambda所在类的this)。函数对象参数有以下形式:
-
空。没有使用任何函数对象参数。
-
=。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
-
&。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
-
this。函数体内可以使用lambda所在类中的成员变量。
-
a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
-
&a。将a按引用进行传递。
-
a, &b。将a按值进行传递,b按引用进行传递。
-
=,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
-
&, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。
② 操作符重载函数参数
标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
③ 可修改标示符
mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
④ 错误抛出标示符
exception声明,这部分也可以省略。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)
⑤ 函数返回值
->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
⑥ 是函数体
{},标识函数的实现,这部分不能省略,但函数体可以为空。
class Test
{
public:
int i = 0;
void func(int x, int y)
{
auto x1 = []{ return i; }; //err, 没有捕获外部变量
auto x2 = [=]{ return i+x+y; }; //ok, 值传递方式捕获所有外部变量
auto x3 = [=]{ return i+x+y; }; //ok, 引用传递方式捕获所有外部变量
auto x4 = [this]{ return i; }; //ok, 捕获this指针
auto x5 = [this]{ return i+x+y; }; //err, 没有捕获x, y
auto x6 = [this, x, y]{ return i+x+y; };//ok, 捕获this指针, x, y
auto x9 = [this]{ return i++; }; //ok, 捕获this指针, 并修改成员的值
}
};
int main()
{
int a = 0, b = 1;
auto f1 = []{ return a; }; //err, 没有捕获外部变量
auto f2 = [=]{ return a; }; //ok, 值传递方式捕获所有外部变量
auto f3 = [=]{ return a++; }; //err, a是以赋值方式捕获的,无法修改
auto f4 = [=]() mutable { return a++; }; //ok, 加上mutable修饰符后,可以修改按值传递进来的拷贝
auto f5 = [&]{ return a++; }; //ok, 引用传递方式捕获所有外部变量, 并对a执行自加运算
auto f6 = [a]{ return a+b; }; //err, 没有捕获变量b
auto f9 = [a,&b]{ return a+(b++); }; //ok, 捕获a, &b
auto f8 = [=,&b]{ return a+(b++); }; //ok, 捕获所有外部变量,&b
return 0;
}
值传递和引用传递区别:
int main()
{
int j = 12;
auto by_val_lambda = [=] { return j + 1;};
auto by_ref_lambda = [&] { return j + 1;};
cout << "by_val_lambda: " << by_val_lambda() << endl;
cout << "by_ref_lambda: " << by_ref_lambda() << endl;
j++;
cout << "by_val_lambda: " << by_val_lambda() << endl;
cout << "by_ref_lambda: " << by_ref_lambda() << endl;
/*
运行结果:
by_val_lambda: 13
by_ref_lambda: 13
by_val_lambda: 13
by_ref_lambda: 14
*/
return 0;
}
第3次调用结果还是13,原因是由于by_val_lambda中,j被视为了一个常量,一旦初始化后不会再改变。
2.3.2 lambda与仿函数
class MyFunctor
{
public:
MyFunctor(int tmp) : round(tmp) {}
int operator()(int tmp) { return tmp + round; }
private:
int round;
};
int main()
{
//仿函数
int round = 2;
MyFunctor f1(round);//调用构造函数
cout << "result1 = " << f1(1) << endl; //operator()(int tmp)
//lambda表达式
auto f2 = [=](int tmp) -> int { return tmp + round; } ;
cout << "result2 = " << f2(1) << endl;
return 0;
}
通过上面的例子,我们看到,仿函数以round初始化类,而lambda函数也捕获了round变量,其它的,如果在参数传递上,两者保持一致。
除去在语法层面上的不同,lambda和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态,并接受参数进行运行。
而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式。
2.3.3 lambda类型
lambda表达式的类型在C++11中被称为“闭包类型”,每一个lambda表达式则会产生一个临时对象(右值)。因此,严格地将,lambda函数并非函数指针。
不过C++11标准却允许lambda表达式向函数指针的转换,但提前是lambda函数没有捕获任何变量,且函数指针所示的函数原型,必须跟lambda函数函数有着相同的调用方式。
int main()
{
//使用std::function和std::bind来存储和操作lambda表达式
function<int(int)> f1 = [](int a) { return a; };
function<int()> f2 = bind([](int a){ return a; }, 123);
cout << "f1 = " << f1(123) << endl;
cout << "f2 = " << f2() << endl;
auto f3 = [](int x, int y)->int{ return x + y; }; //lambda表达式,没有捕获任何外部变量
typedef int (*PF1)(int x, int y); //函数指针类型
typedef int (*PF2)(int x);
PF1 p1; //函数指针变量
p1 = f3; //ok, lambda表达式向函数指针的转换
cout << "p1 = " << p1(3, 4) << endl;
PF2 p2;
p2 = f3; //err, 编译失败,参数必须一致
decltype(f3) p3 = f3; // 需通过decltype获得lambda的类型
decltype(f3) p4 = p1; // err 编译失败,函数指针无法转换为lambda
return 0;
}
2.3.4 lambda优势
#include <vector>
#include <algorithm> //std::for_each
#include <iostream>
using namespace std;
vector<int> nums;
vector<int> largeNums;
class LNums
{
public:
LNums(int u): ubound(u){} //构造函数
void operator () (int i) const
{//仿函数
if (i > ubound)
{
largeNums.push_back(i);
}
}
private:
int ubound;
};
int main()
{
//初始化数据
for(auto i = 0; i < 10; ++i)
{
nums.push_back(i);
}
int ubound = 5;
//1、传统的for循环
for (auto itr = nums.begin(); itr != nums.end(); ++itr)
{
if (*itr > ubound)
{
largeNums.push_back(*itr);
}
}
//2、使用仿函数
for_each(nums.begin(), nums.end(), LNums(ubound));
//3、使用lambda函数和算法for_each
for_each(nums.begin(), nums.end(), [=](int i)
{
if (i > ubound)
{
largeNums.push_back(i);
}
}
);
//4、遍历元素
for_each(largeNums.begin(), largeNums.end(), [=](int i)
{
cout << i << ", ";
}
);
cout << endl;
return 0;
}
lambda表达式的价值在于,就地封装短小的功能闭包,可以及其方便地表达出我们希望执行的具体操作,并让上下文结合更加紧密。
2.3.5表达式捕获
值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。
C++14 给与了我们方便,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获,被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 auto 本质上是相同的:
#include <iostream>
#include <memory> // std::make_unique
#include <utility> // std::move
void lambda_expression_capture() {
auto important = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
return x + y + v1 + (*v2);
};
std::cout << add(3, 4) << std::endl;
}
在上面的代码中, important 是一个独占指针,是不能够被 “=” 值捕获到,这时候我们可以将其转移为右值,在表达式中初始化。
2.3.6泛型 Lambda
auto 关键字不能够用在参数表里,这是因为这样的写法会与模板的功能产生冲突。但是 Lambda 表达式并不是普通函数,所以 Lambda 表达式并不能够模板化。这就为我们造成了一定程度上的麻烦:参数表不能够泛化,必须明确参数表类型。
幸运的是,这种麻烦只存在于 C++11 中,从 C++14 开始, Lambda 函数的形式参数可以使用 auto关键字来产生意义上的泛型:
int main() {
auto add = [](auto x, auto y) {
return x + y;
};
add(1, 2);
add(1.1, 2.2);
}
2.3.7与std::function的使用
#include <iostream>
#include <functional>
int foo(int para) {
return para;
}
int main() {
// std::function 包装了一个返回值为 int, 参数为 int 的函数
std::function<int(int)> func = foo;
int important = 10;
std::function<int(int)> func2 = [&](int value) -> int {
return 1+value+important;
};
std::cout << func(10) << std::endl;
std::cout << func2(10) << std::endl;
}