函数对象

函数对象

实际上就是类中重载了(),以至于使用类对象能像调用函数一样。

先看个最基础的了解一下怎么使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

class Adder {
public:
Adder() = default;

int operator()(int a, int b) {
return a + b;
}
};

int main() {
Adder add;
std::cout << add(1, 1) << '\n'; // 打印 2
std::cout << add(2, 3) << '\n'; // 打印 5

return 0;
}

可以看到,就是把对象当做函数调用。

lambda函数是匿名函数对象的语法糖

函数对象可以结合泛型编程使用,这很正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <algorithm>
#include <array>
#include <vector>

class ListArr {
public:
ListArr() = default;

template <typename ArrT>
void operator()(ArrT& arr) {
std::for_each(arr.begin(), arr.end(),
[](const auto& e) { std::cout << e << ' '; } );
std::cout << std::endl;
}
};

int main() {
std::vector<int> a {1, 2, 3};
std::vector<double> b {4.5, 5.5, 6.5};
std::array<int, 3> c {7, 8, 9};

ListArr la;
la(a); // 输出 1 2 3
la(b); // 输出 4.5 5.5 6.5
la(c); // 输出 7 8 9

return 0;
}

上面的代码用到了c++14支持的lambda泛型,所以需要使用c++14和以上版本的标准编译,现在我基本都使用c++17标准编译自己的代码。

我们上面在函数对象中写了个lambda函数,实际上lambda函数本质上就是个匿名函数对象,lambda函数是个匿名函数对象的语法糖。

我们写的lambda表达式:

1
[](const auto& e) { std::cout << e << ' '; }

编译器生成的匿名函数对象大概是:

1
2
3
4
5
6
7
class __AnonymousLambda {
public:
template<typename T>
void operator()(const T& e) const {
std::cout << e << ' ';
}
};

实际上编译器会生成更完整的内容(局部类或者结构体),让我们的调用能够生效。

函数对象利于内联优化

众所周知,编译器内联优化就是把函数源码填充到调用的地方,这样我们的代码就少了一些地址跳转,函数返回,达到优化的目的,但这个前提是编译器能够知道调用的源码是什么才行。

倘若我们使用函数指针,那编译器在编译期间基本没法知道函数地址,就没法内联优化,而使用函数对象在编译期间是可以确定内容的,当然,用lambda函数也一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename Func>
void repeat(int n, Func f) {
for (int i = 0; i < n; ++i) f(i);
}

struct Printer {
void operator()(int x) const {
std::cout << x << ' ';
}
};

int main() {
repeat(5, Printer()); // 使用函数对象
repeat(5, [](int x){ std::cout << x << ' '; }); // lambda 同理
}

写一个小学二年级都会的斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

template <int N>
class Fib {
public:
constexpr int operator()() const {
if constexpr (N == 0) return 0;
if constexpr (N == 1 || N == 2) {
return 1;
} else {
return Fib<N-1>()() + Fib<N-2>()();
}

}
};

int main() {
std::cout << Fib<3>()() << '\n';
std::cout << Fib<5>()() << '\n';
std::cout << Fib<7>()() << '\n';
std::cout << Fib<9>()() << '\n';
// std::cout << Fib<999>()() << '\n'; // don't do this

return 0;
}

用非类型模板和函数对象可以达到一种诡异的效果,其实也没什么神秘的,就是编译期计算和函数对象调用罢了。

要注意if constexpr是c++17引入的,编译标准需要改到c++17。

上面代码有意思的点在于,去掉{}就会编译错误。

究其原因就是因为这还是在编译期,return之后还是要编译后面的语句,而我们的运算也是在编译期发生的,所以就会发生无限递归,越界。

1
2
3
4
5
template <int N>
constexpr int fib() {
if constexpr (N == 1) return 1;
return fib<N-1>() + fib<N-2>(); // 即使 N == 1 时永远不执行,也必须编译,而编译时又发生计算,便会无穷尽也
}