C++ | 函数对象 & 匿名函数
本文仍为草稿
在 C++ | STL III 的最后, 我们曾简单地谈到了函数对象和匿名函数
函数对象
众所周知, C++ 出现的一个主要目的就是实现面向对象程序设计(Object-Oriented Program), 在先前的三篇有关 STL 的文章中我们详细讨论了什么是对象, 实际上, 早在 C++98 时代, C++ 中的对象的概念就不止限定在类或结构体上面, 函数也可以是 “对象”
C++98 提供了有关函数对象的库 <functional>
, 引入这个库
这个库提供了很多基础的预定义函数对象, 帮助库 <algorithm>
中算法的实现, 包括了内置函数对象, 谓词等
内置函数对象
下面是一个实例, 我们用类型 int 实例化了加减乘除和取模五种运算函数, 调用这些运算函数对象, 和直接使用运算符并没有差别, 但是这种写法更加符合 “泛型” 的概念–即我们可以对不同的对象使用一样的函数, 达到各自的效果
int main(){ |
谓词 Predicate
称返回值类型为 bool 的函数对象为谓词
下面的实例展示了我们如何实例化一个表示等于的谓词
int main(){ |
在最开始, 谓词的出现可能是为了配合 STL 中增删改查等算法的判断环节
在 FDS笔记 - Heap 一文中, 我们曾使用 greater
和 less
两种谓词来自定义堆序, 实际上他们的返回值就是两个参数之间的大小比较关系, 总的来说, 在 C++11 有了明确的函数对象的概念, 以至于有了匿名函数的概念, 谓词的作用可能没有之前那么大了
函数对象举例
下面都可以看作是函数对象
- 函数指针或可以转化为函数指针的类对象
- 对 operator() 的重载(仿函数)
- lambda 表达式 / 匿名函数
- std::bind() 的返回值
在 Python 中, 函数对象的影响力更加显著
因为在 Python 中, 所有东西都是 “对象” (bushi)
myInt = int |
在这个实例中, 我们把 int 函数当作是一个对象赋值给了 myInt, 因此我们可以像使用 int 一样使用 myInt
std::function
为了使函数的调用更加方便, C++11 引入了 std::function, std::bind, 匿名函数
std::function 是对函数对象的封装器, 表示 “函数” 这个抽象概念, 或者表示 “可调用对象” 这个概念
std::function 采用以下声明格式
std::function<returnType(parameterType/* , ... */)> functionObject; |
- returnType: 表示函数的返回值类型, 尽管在 C++ 中, 缺省了返回值类型的函数的返回值类型是 void, 但是在这里是不可缺省的
- parameterType: 表示函数接受的参数的类型
利用我们实例化出来的 functionObject, 我们可以封装同类型的函数
实例
void mySqrtFoo(int x){ |
std::function 还可以用作回调函数, 或者在 C++ 里如果需要使用回调那就一定要使用 std::function
std::bind
使用std::bind可以将函数对象和参数一起绑定, 绑定后的结果使用 std::function / auto 进行保存
- 绑定无参函数
void noParaFoo(){ |
- 绑定带参函数
传参时需要按照顺序传参, 假如需要选择性传参, 需要使用占位符 std::placeholder
void multiParaFoo(int x, int y, int z, int w ){ |
占位符中的 _1, _2 是有实际意义的, 表示自己所管辖的参数按照第 i (i >= 1)个参数传参, 例如:
int main(){ |
- 绑定类成员函数
此时至少需要两个参数, 第一个参数为类的成员函数, 第二个参数为类对象 / 类对象的 this 指针 / 对象地址
class Foo{ |
之所以需要绑定类对象, 是因为这个函数是非静态的, 对于静态类成员函数, 无需绑定类对象
class Foo{ |
std::bind 默认以复制的方式传参, 即使待绑定函数需要的是一个引用, 除非给定的参数是一个右值引用, 如
struct Foo{ |
匿名函数 Lambda Expression
Lambda Expression 引入于 C++11, 用于定义一个匿名函数
auto func = [capture] (paramaters) opt -> returnType { funcBody; }; |
其中:
- capture: 捕获列表, 捕获一定范围的变量并按照一定的规则在函数体中使用
- paramaters: 参数表
- opt: 函数选项
- returnType: 返回值类型, 一般可以缺省, 交由编译器推导
- funcBody: 函数体
捕获列表 Capture
只有参数表和捕获列表包含的变量才能用于匿名函数内部
int main(){ |
- [] 不捕获任何变量
- [&] 捕获外部作用域所有变量, 在函数体内当作引用使用
- [=] 捕获外部作用域所有变量, 在函数内内创建副本后使用
- [=, &a] 值捕获外部作用域所有变量, 引用捕获 a 变量
int main(){
int x = 1;
// auto func1 = [x](){++x;}; // ERROR
auto func2 = [&x](){++x;};
std::cout<<x<<std::endl;
func2();
std::cout<<x<<std::endl;
}
/* output:
1
2
*/ - [a] 只值捕获a变量, 不捕获其它变量
- [this] 捕获当前类中的this指针
struct Node{
int val;
std::function<void(void)> func = [this](){std::cout<<(this->val)<<std::endl;}; // 这里不能使用 auto 推导
// std::function<void(void)> func = [](){std::cout<<(this->val)<<std::endl;}; // ERROR
};
函数选项 Options
观察以下代码
int main(){ |
可以发现 f1 是无法过编的, f2 是可以过编的
这是因为匿名函数的本质是一个匿名类中的 operator(), 这个操作默认是 const 的, 所以我们无法修改 s 并返回 s++, 但是我们可以修改其修饰器为 mutable, 就可以修改成员变量了
本质
上文提到, 匿名函数本质上就是一个匿名类的成员函数 operator(), 所以下面两段代码实现的效果是相同的
匿名函数
int main(){ |
匿名类
struct{ |
实际上, 匿名函数被隐式地定义为内联
但不是所有匿名函数都会被视作内联函数, 只有当编译器觉得它可以内联, 才会作为内联函数使用
使用匿名函数可以减少栈帧的消耗