C++ 仿函数(Functors)

仿函数(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::bindstd::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 设计和模板元编程仍然非常重要。

相关推荐
张健11564096481 天前
使用信号量限制并发数量
开发语言·c++
jc06201 天前
6.1云原生之Docker
c++·docker·云原生
叶子野格1 天前
《C语言学习:指针》12
c语言·开发语言·c++·学习·visual studio
Fuyo_11191 天前
C++ 内存管理
c++·笔记
澈2071 天前
C++面向对象:类与对象核心解析
c++·算法
6Hzlia1 天前
【Hot 100 刷题计划】 LeetCode 141. 环形链表 | C++ 哈希表直觉解法
c++·leetcode·链表
handler011 天前
Linux 进程探索:从 PCB 管理到 fork() 的写时拷贝
linux·c语言·c++·笔记·学习
众少成多积小致巨1 天前
GNU Make 核心指南
android·c++
谭欣辰1 天前
详细讲解 C++ 状压 DP
开发语言·c++·动态规划
William_wL_1 天前
【C++】stack和queue的使用和实现(附加deque的简单介绍)
开发语言·c++