本文仍为草稿

C++ | STL III 的最后, 我们曾简单地谈到了函数对象和匿名函数

函数对象

众所周知, C++ 出现的一个主要目的就是实现面向对象程序设计(Object-Oriented Program), 在先前的三篇有关 STL 的文章中我们详细讨论了什么是对象, 实际上, 早在 C++98 时代, C++ 中的对象的概念就不止限定在类或结构体上面, 函数也可以是 “对象”

C++98 提供了有关函数对象的库 <functional>, 引入这个库

#include<functional>

这个库提供了很多基础的预定义函数对象, 帮助库 <algorithm> 中算法的实现, 包括了内置函数对象, 谓词等

 

内置函数对象

下面是一个实例, 我们用类型 int 实例化了加减乘除和取模五种运算函数, 调用这些运算函数对象, 和直接使用运算符并没有差别, 但是这种写法更加符合 “泛型” 的概念–即我们可以对不同的对象使用一样的函数, 达到各自的效果

int main(){
auto _plus = std::plus<int>(); // OR std::plus<int> _plus;
auto _minus = std::minus<int>();
auto _multiplies = std::multiplies<int>();
auto _divides = std::divides<int>();
auto _modulus = std::modulus<int>();

std::cout<<_plus(7, 5)<<std::endl;
std::cout<<_minus(7, 5)<<std::endl;
std::cout<<_multiplies(7, 5)<<std::endl;
std::cout<<_divides(7, 5)<<std::endl;
std::cout<<_modulus(7, 5)<<std::endl;
}
/* output:
12
2
35
1
2
*/

 

谓词 Predicate

称返回值类型为 bool 的函数对象为谓词

下面的实例展示了我们如何实例化一个表示等于的谓词

int main(){
auto _equal_to = std::equal_to<int>();

std::cout<<_equal_to(9, 9)<<' '<<_equal_to(5, -2);
}
// output: 1 0

在最开始, 谓词的出现可能是为了配合 STL 中增删改查等算法的判断环节

FDS笔记 - Heap 一文中, 我们曾使用 greaterless 两种谓词来自定义堆序, 实际上他们的返回值就是两个参数之间的大小比较关系, 总的来说, 在 C++11 有了明确的函数对象的概念, 以至于有了匿名函数的概念, 谓词的作用可能没有之前那么大了

 

函数对象举例

下面都可以看作是函数对象

  • 函数指针或可以转化为函数指针的类对象
  • 对 operator() 的重载(仿函数)
  • lambda 表达式 / 匿名函数
  • std::bind() 的返回值

在 Python 中, 函数对象的影响力更加显著

因为在 Python 中, 所有东西都是 “对象” (bushi)

myInt = int
print(myInt(2.4))
# output: 2

在这个实例中, 我们把 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){
int y = x * x;
std::cout<<y<<", ";
}

struct Node{
int val;
void operator()(int x){
mySqrtFoo(x);
}
};

int main(){
std::function<void(int)> mySqrt = mySqrtFoo; // 封装自由函数
mySqrt(4);

std::function<void()> mySqrt5 = []() {mySqrtFoo(5);}; // 封装匿名函数
mySqrt5();

std::function<void()> mySqrt39 = std::bind(mySqrtFoo, 39); // 封装 std::bind() 的返回值
mySqrt39();

std::function<void(int)> mySqrtNode = Node(); // 封装 operator()
mySqrtNode(114);
}
// output: 16, 25, 1521, 12996,

std::function 还可以用作回调函数, 或者在 C++ 里如果需要使用回调那就一定要使用 std::function

 

std::bind

使用std::bind可以将函数对象和参数一起绑定, 绑定后的结果使用 std::function / auto 进行保存

  1. 绑定无参函数
void noParaFoo(){
std::cout<<"No Parameter"<<std::endl;
}

int main(){
auto noPara = std::bind(noParaFoo);
noPara();
}
// output: No Parameter
  1. 绑定带参函数

传参时需要按照顺序传参, 假如需要选择性传参, 需要使用占位符 std::placeholder

void multiParaFoo(int x, int y, int z, int w ){
std::cout<<x<<'#';
std::cout<<y<<'#';
std::cout<<z<<'#';
std::cout<<w<<'#';
std::cout<<std::endl;
}

int main(){
auto multiPara1 = std::bind(multiParaFoo,
100,
std::placeholders::_1,
300,
std::placeholders::_2);
multiPara1(2, 4);
}
// output: 100#2#300#4#

占位符中的 _1, _2 是有实际意义的, 表示自己所管辖的参数按照第 i (i >= 1)个参数传参, 例如:

int main(){
auto multiPara2 = std::bind(multiParaFoo,
std::placeholders::_3,
std::placeholders::_1,
std::placeholders::_1,
std::placeholders::_2);
multiPara2(3, 2, 4);
}
// output: 4#3#3#2#
  1. 绑定类成员函数

此时至少需要两个参数, 第一个参数为类的成员函数, 第二个参数为类对象 / 类对象的 this 指针 / 对象地址

class Foo{
public:
void printHelloFoo(){
std::cout<<"Hello, Vanadium!"<<std::endl;
}
};

int main(){
Foo foo;
auto printHello = std::bind(&Foo::printHelloFoo, foo);
printHello();
}
// output: Hello, Vanadium!

之所以需要绑定类对象, 是因为这个函数是非静态的, 对于静态类成员函数, 无需绑定类对象

class Foo{
public:
int id = 1;
void printHelloFoo(){
std::cout<<"Hello, "<<id<<std::endl;
}
static void staticPrintFoo(){
std::cout<<"Static!"<<std::endl;
}
};

int main(){
Foo foo, foo_;
foo_.id = 2;
auto printHello = std::bind(&Foo::printHelloFoo, foo);
auto printHello_ = std::bind(&Foo::printHelloFoo, foo_);
// auto staticPrint = std::bind(&Foo::staticPrintFoo, foo); // Right, but not good
auto staticPrint = std::bind(&Foo::staticPrintFoo); // Well
printHello();
printHello_();
staticPrint();
}
/* output:
Hello, 1
Hello, 2
Static!
*/

std::bind 默认以复制的方式传参, 即使待绑定函数需要的是一个引用, 除非给定的参数是一个右值引用, 如

struct Foo{
Foo(){}
Foo(const Foo&){
std::cout<<"copy_cstr"<<std::endl;
}
Foo(const Foo&&){
std::cout<<"move_cstr"<<std::endl;
}
};

void f(Foo x){}
void rf(Foo& x){}

int main(){
Foo foo;
Foo& rfoo = foo;
std::bind(f, foo);
std::bind(rf, foo);
std::bind(f, rfoo);
std::bind(rf, rfoo);
std::bind(f, std::move(foo));
std::bind(rf, std::move(rfoo));
}
/* output:
copy_cstr
copy_cstr
copy_cstr
copy_cstr
move_cstr
move_cstr
*/

 

匿名函数 Lambda Expression

Lambda Expression 引入于 C++11, 用于定义一个匿名函数

auto func = [capture] (paramaters) opt -> returnType { funcBody; };

其中:

  • capture: 捕获列表, 捕获一定范围的变量并按照一定的规则在函数体中使用
  • paramaters: 参数表
  • opt: 函数选项
  • returnType: 返回值类型, 一般可以缺省, 交由编译器推导
  • funcBody: 函数体

 

捕获列表 Capture

只有参数表和捕获列表包含的变量才能用于匿名函数内部

int main(){
int a = 1, b = 2;

auto func1 = [](){std::cout<<(a + 1)<<std::endl;}; // ERROR
auto func2 = [a](){std::cout<<(b + 1)<<std::endl;}; // ERROR
auto func3 = [a](){std::cout<<(a + 1)<<std::endl;}; // RIGHT
}
  • [] 不捕获任何变量
  • [&] 捕获外部作用域所有变量, 在函数体内当作引用使用
  • [=] 捕获外部作用域所有变量, 在函数内内创建副本后使用
  • [=, &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(){
int s = 1;
auto f1 = [=](){return s++;}; // ERROR
auto f2 = [=]()mutable{return s++;}; // RIGHT
}

可以发现 f1 是无法过编的, f2 是可以过编的

这是因为匿名函数的本质是一个匿名类中的 operator(), 这个操作默认是 const 的, 所以我们无法修改 s 并返回 s++, 但是我们可以修改其修饰器为 mutable, 就可以修改成员变量了

 

本质

上文提到, 匿名函数本质上就是一个匿名类的成员函数 operator(), 所以下面两段代码实现的效果是相同的

匿名函数

int main(){
auto T = [](){std::cout<<"Vanadium"<<std::endl;};
T();
}

匿名类

struct{
void operator()(){
std::cout<<"Vanadium"<<std::endl;
}
} T;

int main(){
T();
}

实际上, 匿名函数被隐式地定义为内联

但不是所有匿名函数都会被视作内联函数, 只有当编译器觉得它可以内联, 才会作为内联函数使用

使用匿名函数可以减少栈帧的消耗