一、函数对象的本质

函数对象(也称为仿函数,Functor)是*重载了函数调用运算符***operator()**的类或结构体的实例。这种特殊的设计使它能够像普通函数一样被调用,同时又具备对象的所有特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 一个简单的函数对象类
struct Add {
// 重载函数调用运算符
int operator()(int a, int b) const {
return a + b;
}
};

// 使用方式
int main() {
Add add;
int result = add(3, 5); // 像函数一样调用对象
// 也可以直接使用临时对象
int result2 = Add()(10, 20);
return 0;
}

从本质上讲,函数对象是一个带行为的对象,而普通函数是一段可执行代码。这种本质差异决定了它们在功能和适用场景上的不同。

二、函数对象与普通函数的核心区别

2.1 状态管理能力

这是两者最根本的区别:

  • 普通函数:无法保存状态,除非使用静态变量(但静态变量属于函数本身,所有调用共享同一状态)

  • 函数对象:通过成员变量保存状态,每个实例拥有独立的状态

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
30
#include <iostream>

// 带状态的函数对象
struct Counter {
int count;

Counter() : count(0) {}

int operator()() {
return ++count; // 每次调用更新状态
}
};

// 普通函数(使用静态变量模拟状态)
int counter_func() {
static int count = 0; // 所有调用共享此状态
return ++count;
}

int main() {
// 函数对象可以有多个独立状态的实例
Counter c1, c2;
std::cout << "c1: " << c1() << ", " << c1() << "\n"; // 1, 2
std::cout << "c2: " << c2() << ", " << c2() << "\n"; // 1, 2

// 普通函数的状态是共享的
std::cout << "func: " << counter_func() << ", " << counter_func() << "\n"; // 1, 2

return 0;
}

2.2 泛型能力

  • 普通函数:类型固定,如需支持多种类型需编写多个重载版本

  • 函数对象:可通过模板实现泛型,一个类即可支持多种数据类型

1
2
3
4
5
6
7
8
9
10
11
// 泛型函数对象
template <typename T>
struct Multiply {
T operator()(const T& a, const T& b) const {
return a * b;
}
};

// 普通函数需要重载才能支持多种类型
int multiply(int a, int b) { return a * b; }
double multiply(double a, double b) { return a * b; }

2.3 作为参数传递时的差异

  • 普通函数:作为参数传递时退化为函数指针,可能无法被编译器内联优化

  • 函数对象:作为模板参数传递时,编译器能明确知道具体类型,更容易进行优化

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
#include <vector>
#include <algorithm>

// 函数对象作为算法参数
struct Square {
void operator()(int& x) const {
x = x * x;
}
};

// 普通函数作为算法参数
void square(int& x) {
x = x * x;
}

int main() {
std::vector<int> nums = {1, 2, 3, 4};

// 使用函数对象
std::for_each(nums.begin(), nums.end(), Square());

// 使用普通函数
std::for_each(nums.begin(), nums.end(), square);

return 0;
}

2.4 存储与生命周期

  • 普通函数:代码存储在代码段,程序启动时加载,生命周期与程序相同

  • 函数对象:作为对象存储在栈或堆中,遵循对象的生命周期管理规则,可动态创建和销毁

三、适合使用函数对象的场景

3.1 需要携带状态的操作

当操作需要在多次调用之间维护状态信息时,函数对象是理想选择:

  • 累计计算(如统计元素出现次数、求和时记录中间结果)

  • 带参数的过滤或转换操作(参数可作为函数对象的成员变量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 统计满足条件元素个数的函数对象
template <typename T>
struct ConditionCounter {
int count;
T threshold;

ConditionCounter(T t) : count(0), threshold(t) {}

// 统计大于阈值的元素
void operator()(T elem) {
if (elem > threshold) {
count++;
}
}
};

3.2 作为算法的自定义策略

标准库算法(如sort、find_if)允许传入自定义策略,函数对象可封装复杂的比较逻辑:

  • 自定义排序规则(如按字符串长度排序、按对象的某个成员变量排序)

  • 复杂过滤条件(如同时判断元素是否大于某个值且为偶数)

1
2
3
4
5
6
7
8
9
struct StrLenCmp {
bool operator()(const std::string& a, const std::string& b) {
return a.size() < b.size(); // 按字符串长度排序
}
};

// 用于sort算法:
std::vector<std::string> strs = {"apple", "cat", "banana"};
std::sort(strs.begin(), strs.end(), StrLenCmp());

3.3 可适配性与模板编程

函数对象可作为模板参数,与标准库中的适配器(如bind、not1)配合使用,灵活组合出更复杂的逻辑:

1
2
3
4
5
6
#include <functional>

// 配合标准库适配器
std::vector<int> nums = {3,1,4,2};
// 使用greater比较器(函数对象),排序后降序
std::sort(nums.begin(), nums.end(), std::greater<int>());

3.4 性能敏感的高频调用

函数对象的调用在编译期即可确定(静态绑定),编译器可能会对其进行内联优化,减少函数调用开销,特别适合:

  • 高性能计算中的循环操作

  • 算法内部的高频回调

四、如何实现一个仿函数类

实现仿函数类遵循以下基本步骤:

  1. 定义一个类或结构体
  2. 重载函数调用运算符operator()
  3. 根据需要添加成员变量存储状态
  4. 实现必要的构造函数和成员函数

4.1 基础仿函数类实现

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

// 1. 定义仿函数类
class Adder {
public:
// 2. 重载函数调用运算符
int operator()(int a, int b) const {
return a + b;
}
};

int main() {
Adder adder;
std::cout << "3 + 5 = " << adder(3, 5) << std::endl;
return 0;
}

4.2 带状态的仿函数类

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

class Counter {
private:
int _count;
int _step; // 步长

public:
Counter(int initial = 0, int step = 1)
: _count(initial), _step(step) {}

int operator()() {
int current = _count;
_count += _step;
return current;
}

void reset(int value = 0) {
_count = value;
}
};

4.3 泛型仿函数类

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

template <typename T>
class Multiplier {
private:
T _factor; // 乘数因子

public:
explicit Multiplier(T factor) : _factor(factor) {}

T operator()(const T& value) const {
return value * _factor;
}
};

// 使用
int main() {
Multiplier<int> int_multiplier(3);
Multiplier<double> double_multiplier(2.5);
return 0;
}

4.4 用于 STL 算法的仿函数

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

// 用于过滤的仿函数:检查字符串是否包含指定子串
class StringContains {
private:
std::string _substr;

public:
explicit StringContains(std::string substr) : _substr(std::move(substr)) {}

bool operator()(const std::string& str) const {
return str.find(_substr) != std::string::npos;
}
};

// 使用
// std::find_if(words.begin(), words.end(), StringContains("err"));

五、函数对象与 lambda 表达式的关系

C++11 引入的 lambda 表达式本质上是匿名函数对象,它结合了普通函数的简洁性和函数对象的特性:

1
2
3
4
5
6
// lambda表达式(匿名函数对象)
auto add = [](int a, int b) { return a + b; };

// 带状态的lambda(捕获外部变量)
int threshold = 5;
auto count_above = [threshold](int num) { return num > threshold; };

在简单场景下,lambda 表达式可以替代显式定义的函数对象,而复杂场景(如需要复用或维护复杂状态)仍需使用显式函数对象类。