本节将对函数对象,匿名函数对象(lamda表达式) 以及 匿名函数对象和 函数模版结合对一些用法 做个笔记。同时,分析以下lamda表达式相比于传统函数对象的优劣。
关键是看一些 rocksdb类似的经典C++项目,老是有些细节get不到,导致最终理解的设计思想和实际的实现有差异。所以将这些高阶语法 再系统过一遍,加深对源码的理解。
函数对象在C++98的时候已经比较成熟了,先看一段代码
struct adder {
adder(int n) : n_(n) {}
int operator() (int x)const {
return x + n_;
}
private:
int n_;
};
这是一个函数对象的定义,通过adder
能够实例化一个函数,auto add_3 = adder(3)
,那么add_3就可以作为一个+3的函数来使用。
auto add_3 = adder(3); // c++11 的初始化
adder add_3(3); // c++98的初始化
cout << " 5 add_3's result is " << add_3(5) << endl;
这里为什么add_3能够使用()
括号的语法,就是因为adder类中定义了一个operator ()
。
同时C++98中也在
定义了一些高介函数,来支持函数对象的创建。比较典型的有bind1st
和bind2nd
(由
auto add_3 = bind2nd(plus<int>(), 3); //c++11 的语法
binder2nd<plus<int>()> add_2(plus<int>(), 3); // c++98的语法
将3作为的第二个参数绑定到函数对象plus
如下代码,C++98中直接构造函数对象来为每个数组元素+3
// 为数组中的 每个元素 + 3
vector <int> arr = {1,2,3,4,5};
transform(arr.begin(), arr.end(),
arr.begin(),
bind2nd(plus<int>(),3));
如上add_3
函数的功能可以 通过lamda写出如下的实现方法
auto add_3 = [](int x) {
return x + 3;
};
cout << add_3(5) << endl;
同时如果想要实现一个通用的类似于上文中的addr类,则可以有如下实现
auto adder = [](int x) {
return [x](int n) {
return x + n;
};
};
cout << adder(3)(5) << endl; // 返回 3 + 5
以上代码通过x 来捕获变量x的数值,return的操作就是将 x + n的结果放到x中,然后被外层的x捕获。
第一个优点,之前的案例也能够体现,将加法功能函数进行封装,并能能够立即返回计算的数值。
auto res = [](int x) {
return x * x;
}(9);
cout << res << endl; // 9的平方
第二优点,可以先看如下案例
Obj obj; // 默认构造函数
init_mode=1;
switch (init_mode)
{
case 1:
obj = Obj(2); // 带参数的构造函数(我们真正想要调用的) + 赋值的构造函数
break;
default:
break;
}
我们想要根据输入,构造对应的obj对象,整个过程会调用Obj类的默认构造函数,带参数的构造函数,赋值构造函数。
那么以上代码可以通过lamda表达式来简化,并且不需要默认构造函数和赋值构造函数的参与,仅仅完成参数构造即可返回。
auto obj_lamda = [init_mode]() {
switch (init_mode)
{
case 1:
return Obj(2);
break;
default:
break;
}
}();
完整测试代码如下:
#include
#include
#include
#include
#include
using namespace std;
class Obj {
public:
Obj(){cout << "default construct obj " << endl;}
Obj(int x) : x_(x) {cout << "paramter construct obj" << endl;}
Obj(Obj &&obj) {
cout << "move construct obj" << endl;
this->x_ = obj.x_;
}
Obj& operator=(const Obj &obj) {
this->x_ = obj.x_;
cout << "assign construct obj " << endl;
return *this;
}
~Obj() {}
private:
int x_;
};
int main() {
int init_mode = 1;
cout << "ordinary construct : " << endl;
Obj obj;
switch (init_mode)
{
case 1:
obj = Obj(2);
break;
default:
break;
}
cout << "lamda construct : " << endl;
auto obj_lamda = [init_mode]() {
switch (init_mode)
{
case 1:
return Obj(2);
break;
default:
break;
}
}();
return 0;
}
输出如下:
ordinary construct :
default construct obj
paramter construct obj
assign construct obj
lamda construct :
paramter construct obj
以上的lamda表达式的案例中,lamda后面的[]中传入参数,可以在其后{}的函数体中捕获。
接下来看一下捕获过程的一些细节:
变量捕获的开头是可选的捕获符 =
和 &
, 其中=
是默认的捕获符。 这个捕获的过程是自动按值=
或引用&
捕获用到的本地变量,后面可以跟,
进行分隔。
&
加本地变量名,标明对其按引用捕获(不能在默认捕获符&后出现,因其已自动按引用捕获)这里需要注意,一般在使用按引用捕获 某个变量的时候需要有下面的需求:
举例1: 按引用捕获变量
按引用捕获变量 v1,v2,并修改其值。
vector <int> v1;
vector <int> v2;
// ...
auto push_data = [&](int x) {
//这里也可以使用 [&v1,&v2]进行捕获
v1.push_back(x);
v2.push_back(y);
};
push_data(2);
push_data(3);
举例2: 按值捕获变量
查看如下代码,使用多个线程进行各自对象的复制,从而支持独立运算。
#include
#include
#include
#include
using namespace std;
int get_count() {
static int count = 0;
return ++count;
}
class Task {
public:
Task(int data): data_(data) {}
auto lazy_lunch() {
return
[*this, count = get_count()] () // 按值捕获外部对象
mutable { //mutable 标记捕获的内容可以更改
ostringstream oss;
oss << "Done work " << data_
<< " (No. " << count
<< ") in thread "
<< this_thread::get_id()
<< "\n";
msg_ = oss.str(); //更改来msg_的值
caculate();
};
}
void caculate() {
this_thread::sleep_for(100ms);
cout << msg_;
}
private:
int data_;
string msg_;
};
int main() {
auto t = Task{11};
thread t1 {t.lazy_lunch()};
thread t2 {t.lazy_lunch()};
t1.join();
t2.join();
return 0;
}
输出如下:
#捕获了this的引用,即this的修改可以被外部观察。
#两个不同的线程对象修改了各自的msg_的值,所以打印的msg_的地址内容 不同
Done work 11 (No. 1) in thread 0x70000def5000
Done work 11 (No. 2) in thread 0x70000df78000
注意这段代码需要使用c++14的标准进行编译,因为lamda 表达式中的*this
和count = get_count()
都是14标准中的特性。
以上代码使用了lamda表达式的几个特性:
mutable
标记捕获的内容可以被更改[*this]
表示按值捕获外围对象(Task)[count = get_count()]
捕获表达式可以生成lamda表达式时计算并存储等号后的表达式结果。即将get_count函数执行完,并将返回的结果赋值给count保存下来。如果以上代码我们 针对this
按值捕获[this]
,则不会更改msg_
的地址
#捕获了this的值,即使不同的线程对象在内部更改了msg_的内容,也不会被外部观察到。
# 所以打印的 msg_ 的地址相同,也即Task 初始化时msg_的地址
Done work 11 (No. 2) in thread 0x700008cf5000
Done work 11 (No. 2) in thread 0x700008cf5000
函数的返回值可以是auto,但是参数需要一一声明。
这个过程在lamda表达式中进一步简化,声明参数是可以直接使用auto(包括auto &&),整体也就是类似于自己声明了模版。主要还是因为lamda表达式的参数中无法使用tmplate
关键字。
如下案例
template <typename T, typename V>
auto sum(T t, V v) {
return t + v;
}
跟上面函数等价的lamda表达式是:
auto sum = [](auto t, auto v) {
return x + v;
};
为什么要推出lamda表达式的泛型,还是为了组合性的提升,以上lamda表达式 案例中的sum
就类似于标准库中plus
函数模版。函数对象可以 传递给其他接收函数对象的函数,而 +
则无法完成这个操作。
#include
#include
#includ
using namespace std;
int main(){
std::array<int,5> a{1,2,3,4,5};
auto s = accumulate( // 这个数学函数是进行累加和的操作
a.begin(), a.end(), 1,
[](auto x, auto y) {
return x + y;
}
);
cout << s << endl;
return 0;
}
如以上案例,使用lamda表达式构造 作为 累加和的数学函数accumulate
的一个参数,完成所有传入数值的相加。
但是当lamda的行为发生变化,将return x + y
改为return x *y
,则就变成了计算5的阶乘的操作,即使这个函数是进行累加和的运算。
之前说过,lamda表达式的语法中每一个lamda表达式都由一个全局唯一的类型,所以只能用auto 或 模版参数来接收结果。但很多时候需要这个接收的过程更为通用,所以需要function模版。
function 模版的参数就是函数的类型,一个函数对象放到fucntion里之后,外界只能观察到其参数、返回值类型和执行结果。
ps :function模版的创建非常消耗资源,所以如果auto解决不了的时候再使用函数模版
举例如下:
这个例子中,需要将函数对象放到一个map中,所以需要使用function 函数模版来完成。
std::map<string ,function<int(int, int)>>
op_dict{
{"+",
[](int x, int y) {
return x + y;
}
},
{"-",
[](int x, int y) {
return x - y;
}
},
{"*",
[](int x, int y) {
return x * y;
}
},
{ "/",
[](int x ,int y) {
assert(y!=0);
return x / y;
}
}
};
最终可以通过调用op_dict["+"]\(1,2)
的方式 ,完成1 + 2 的计算,这种方式一般对表达式对解析会比较有用。