函数对象
概述
函数对象是一个可以被当作函数来用的对象。
根据惯例,使用了 struct 关键字而不是 class 关键字:
struct funObjAdd
{
public:
funObjAdd(){}
funObjAdd(int _n) : n(_n){}
// 定义 operator()运算符允许像调用函数一样使用小括号的语法。
int operator()(int i) const
{
return n + i;
}
private:
int n{ 0 };
};
funObjAdd f1(1); //C++98
auto f2 = funObjAdd(1); //C++11
函数指针和引用
除非用一个引用模板参数来捕捉函数类型,传递给一个函数的函数实参会退化成为一个函数指针。不管是函数指针还是函数引用,都可以当成函数对象来用。
int fun(int x)
{
return x * x;
}
template <typename F>
auto test1(F f)
{
return f(3);
}
template <typename F>
auto test2(F& f)
{
return f(3);
}
template <typename F>
auto test3(F* f)
{
// return f(3) is OK
return (*f)(3);
}
cout << test1(fun) << endl; //int (*)(int)
cout << test2(fun) << endl; //int (&)(int)
cout << test3(fun) << endl; //int (*)(int)
注意在函数指针的情况下,直接写 *value 也可以。
很多接收函数对象的地方,也可以接收函数的指针或引用。但在个别情况下,需要通过函数对象的类型来区分函数对象的时候,就不能使用函数指针或引用了——原型相同的函数,它们的类型也是相同的。
Lambda 表达式
Lambda 表达式以一对中括号开始
跟函数定义一样,有参数列表
跟正常的函数定义一样,会有一个函数体,里面会有 return 语句
一般不需要说明返回值(相当于 auto);有特殊情况要说明,使用箭头语法:
[](int x) -> int { … }
每个 lambda 表达式都有一个全局唯一类型,要精确捕捉 lambda 表达式到一个变量中,只能 auto 声明的方式
lambda 本质不是函数指针,可以认为编译器组合出了一个唯一的类型名称。
每个 lambda 都有自己的独特类型,每次定义相当于编译器帮你产生了一个函数对象(就像定义的那些函数对象一样)。
下面是一个lambda表达式反汇编的部分代码:(1f9b1ea5c4d835764b4a74b83856e332像MD5算法生成的)
00A51C78 call <lambda_1f9b1ea5c4d835764b4a74b83856e332>::operator() (0A518E0h)
// 加法
auto my_add = [](int a) {return [a](int b) {return a + b; }; };
cout << my_add(1)(2) << endl;
// vector遍历
vector<int> v{ 2,3,4};
transform(v.begin(), v.end(), v.begin(), [](int a) {return a * a; }); //4,9,16
lambda 表达式可以立即进行求值,它免去了我们定义一个 constexpr 函数的必要。
只要能满足 constexpr 函数的条件,一个 lambda 表达式默认就是 constexpr 函数。
[](int x) { return x * x + x; }(3);
反汇编可以查看到寄存器eax的值为12。
[](int x) { return x * x + x; }(3);
00A51C68 xor eax,eax ;12
00A51C6A mov byte ptr [ebp-0C5h],al
00A51C70 push 3
00A51C72 lea ecx,[ebp-0C5h]
00A51C78 call <lambda_1f9b1ea5c4d835764b4a74b83856e332>::operator() (0A518E0h)
lambda 解决多重初始化路径。
Obj obj;
switch (init_mode) {
case init_mode1:
obj = Obj(...);
break;
case init_mode2;
obj = Obj(...);
break;
...
}
上面实际上是调用了默认构造函数、带参数的构造函数和(移动)赋值函数:既可能有性能损失,也对 Obj 提出了有默认构造函数的额外要求。对于这样的代码,有一种重构意见是把这样的代码分离成独立的函数。不过,有时候更直截了当的做法是用一个 lambda 表达式来进行改造,既可以提升性能(不需要默认函数或拷贝 / 移动),又让初始化部分显得更清晰:
auto obj = [init_mode]() {
switch (init_mode) {
case init_mode1:
return Obj(...);
break;
case init_mode2:
return Obj(...);
break;
...
}
}();
变量捕获细节
变量捕获开头是可选的默认捕获符 = 或 &,会自动按值或按引用捕获用到的本地变量,后面跟(逗号分隔):
- 本地变量名标明对其按值捕获(不能在默认捕获符 = 后,因其已自动按值捕获所有本地变量)
- & 加本地变量名标明对其按引用捕获(不能在默认捕获符 & 后,因其已自动按引用捕获所有本地变量)
- this 标明按引用捕获外围对象(对 lambda 表达式定义出现在一个非静态类成员内的情况);注意默认捕获符 = 和 & 号可以自动捕获 this(并且在 C++20 之前,在 = 后写 this 会导致出错)
- *this 标明按值捕获外围对象(对 lambda 表达式定义出现在一个非静态类成员内的情况;C++17)
- 变量名 = 表达式 标明按值捕获表达式的结果(可理解为 auto 变量名 = 表达式)
- &变量名 = 表达式 标明按引用捕获表达式的结果(可理解为 auto& 变量名 = 表达式)
一般而言,按值捕获是比较安全的做法。按引用捕获时则需要更小心些,必须能够确保被捕获的变量和 lambda 表达式的生命期至少一样长,并在有下面需求之一时才使用:
- 需要在 lambda 表达式中修改这个变量并让外部观察到
- 需要看到这个变量在外部被修改的结果
- 这个变量的复制代价比较高
如果希望以移动的方式来捕获某个变量的话,则应考虑 变量名 = 表达式 的形式。表达式可以返回一个 prvalue 或 xvalue,比如可以是 std::move(需移动捕获的变量)。
泛型 lambda 表达式
函数的返回值可以 auto,但参数还是要声明的。在 lambda 表达式里则更进一步,在参数声明时就可以使用 auto(包括 auto&& 等形式)。
template <typename T, typename V>
auto add(T t, V v)
{
return t + v;
}
auto add = [](auto t, auto v)
{
return t + v;
};
bind
bind1st 和 bind2nd 目前已经从 C++ 标准里移除,原因:
- 功能可以被 lambda 表达式替代
- 更强大的 bind 模板
vector<int> v{ 2,3,4};
transform(v.begin(), v.end(), v.begin(), bind(plus<int>(), placeholders::_1, 1));//3,4,5
bind功能可以被 lambda 表达式替代,对 bind 只需要稍微了解一下就好,在 C++14 之后的年代里,已经没有什么地方必须要使用 bind 了。
function
每一个 lambda 表达式都是一个单独的类型,所以只能使用 auto 或模板参数来接收结果。在很多情况下,我们需要使用一个更方便的通用类型来接收,这时就可以使用 function 模板。function 模板的参数就是函数的类型,一个函数对象放到 function 里之后,外界可以观察到的就只剩下它的参数、返回值类型和执行效果了。
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) { return x / y; }},
};
注意 function 对象的创建还是比较耗资源的,所以只在用 auto 等方法解决不了问题的时候使用这个模板。
- 本文链接:https://morisa66.github.io/2021/03/01/FunctionObject/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。