仿函数(Functors)详解
1. 什么是仿函数?
仿函数(Function Object)是一个重载了函数调用运算符 operator() 的类对象。使用起来像普通函数,但实际上是一个对象,因此可以拥有状态。
cpp
struct MyFunctor {
void operator()(int x) const {
std::cout << x << std::endl;
}
};
MyFunctor f;
f(42); // 看起来像调用函数,实际是调用 f.operator()(42)
2. 为什么需要仿函数?
相比普通函数,仿函数有以下优势:
- 可以携带状态:类成员变量可以在多次调用之间保持信息。
- 可以作为类型参数:仿函数是一个类类型,可以作为模板参数传递,编译器可以在编译时内联调用,性能更高。
- 可内联 :函数指针通常无法内联(编译器很难确定指向哪个函数),而仿函数的
operator()可以是内联的,提升效率。 - 可以组合:通过配接器(adapters)可以组合多个仿函数。
3. 如何编写仿函数?
一个简单的仿函数例子:
cpp
// 加法仿函数
struct Add {
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Add add;
int result = add(3, 4); // result = 7
return 0;
}
带状态的仿函数:
cpp
// 计数器仿函数,每次调用递增
class Counter {
int count = 0;
public:
int operator()() {
return ++count;
}
};
Counter c;
std::cout << c() << std::endl; // 1
std::cout << c() << std::endl; // 2
4. 在STL算法中使用仿函数
STL算法通过模板参数接受仿函数,实现策略定制。
例1:std::sort 使用仿函数自定义排序
cpp
#include <algorithm>
#include <vector>
#include <iostream>
struct Descending {
bool operator()(int a, int b) const {
return a > b;
}
};
int main() {
std::vector<int> v = {3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), Descending());
for (int x : v) std::cout << x << " "; // 5 4 3 1 1
}
例2:std::for_each 使用仿函数修改元素并统计
cpp
#include <algorithm>
#include <vector>
#include <iostream>
struct MultiplyBy {
int factor;
MultiplyBy(int f) : factor(f) {}
void operator()(int& x) const {
x *= factor;
}
};
int main() {
std::vector<int> v = {1, 2, 3, 4};
std::for_each(v.begin(), v.end(), MultiplyBy(10));
// v 变为 {10, 20, 30, 40}
}
例3:std::find_if 使用仿函数查找满足条件的元素
cpp
#include <algorithm>
#include <vector>
struct IsEven {
bool operator()(int x) const {
return x % 2 == 0;
}
};
int main() {
std::vector<int> v = {1, 3, 5, 6, 7};
auto it = std::find_if(v.begin(), v.end(), IsEven());
if (it != v.end()) {
std::cout << "First even number: " << *it << std::endl; // 6
}
}
5. STL 预定义的仿函数
STL 提供了大量常用仿函数,定义在 <functional> 头文件中:
| 分类 | 仿函数 | 作用 |
|---|---|---|
| 算术运算 | plus<T>, minus<T>, multiplies<T>, divides<T>, modulus<T>, negate<T> |
加、减、乘、除、取模、取反 |
| 比较运算 | equal_to<T>, not_equal_to<T>, greater<T>, less<T>, greater_equal<T>, less_equal<T> |
等于、不等、大于、小于、大于等于、小于等于 |
| 逻辑运算 | logical_and<T>, logical_or<T>, logical_not<T> |
与、或、非 |
| 位运算 | bit_and<T>, bit_or<T>, bit_xor<T> |
与、或、异或 |
示例:
cpp
#include <functional>
#include <iostream>
int main() {
std::plus<int> add;
std::greater<int> cmp;
std::cout << add(10, 20) << std::endl; // 30
std::cout << cmp(30, 20) << std::endl; // 1 (true)
return 0;
}
6. 仿函数 vs 函数指针
| 特性 | 仿函数 | 函数指针 |
|---|---|---|
| 状态 | 可以拥有成员变量保存状态 | 无状态,只能依靠静态局部变量(线程不安全) |
| 内联可能性 | 高,编译器知道具体类型 | 低,通常不能内联 |
| 类型安全 | 强类型,每个仿函数是独立类型 | 类型由函数签名决定 |
| 作为模板参数 | 可以直接传递类型 | 需要传递函数指针值,通常需要额外包装 |
| 性能 | 通常更快(内联机会多) | 略慢(间接调用) |
| 灵活性 | 高,可包含多个成员函数 | 单一功能 |
7. 仿函数配接器(Function Adapters)
STL 提供配接器用于组合或修改仿函数:
bind1st,bind2nd(C++11 弃用,推荐std::bind):绑定一个参数。not1,not2:对一元/二元仿函数的结果取反。ptr_fun(已弃用):将普通函数适配成仿函数。
C++11 后,推荐使用 std::bind 和 std::function 以及 lambda 表达式。
8. 现代 C++ 中的仿函数:Lambda 表达式
从 C++11 开始,lambda 表达式可以看作匿名仿函数的语法糖。编译器会将 lambda 转换为一个未命名的仿函数类。
cpp
// 传统仿函数
struct IsOdd {
bool operator()(int x) const { return x % 2 == 1; }
};
std::count_if(v.begin(), v.end(), IsOdd());
// Lambda 等价写法
std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 1; });
Lambda 可以捕获外部变量,相当于带状态的仿函数:
cpp
int threshold = 5;
auto greater_than = [threshold](int x) { return x > threshold; };
// 编译器生成类似下面的仿函数类:
/*
class Anonymous {
int threshold;
public:
Anonymous(int t) : threshold(t) {}
bool operator()(int x) const { return x > threshold; }
};
*/
9. 仿函数的缺点与注意事项
- 代码膨胀:每个仿函数是一个类型,大量使用可能导致模板实例化增多,增加编译时间。
- 可读性:对于简单操作,lambda 更简洁直观。
- C++17 后
std::unary_function/std::binary_function被弃用 :早期版本中,仿函数常继承这些基类以获取argument_type等 typedef,现在不需要了。
10. 总结
仿函数是 C++ 泛型编程的重要工具,它将行为封装为对象,提供比函数指针更强的灵活性、性能和状态管理能力。在现代 C++ 中,简单场景下 lambda 表达式替代了大部分仿函数的手写需求,但理解仿函数的原理对于掌握 STL 设计和模板元编程仍然非常重要。