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 设计和模板元编程仍然非常重要。

相关推荐
鲸渔2 小时前
【C++ 入门】第一个程序:Hello World 与基本语法规则
开发语言·c++·算法
会编程的土豆2 小时前
【数据结构与算法】 时间复杂度计算
数据结构·c++·算法
John_ToDebug2 小时前
Chromium 页面类型与 IPC 通信机制深度解析
前端·c++·chrome
小年糕是糕手2 小时前
【35天从0开始备战蓝桥杯 -- Day9】
数据结构·数据库·c++·算法·蓝桥杯
山甫aa2 小时前
STL---常见数据结构总结
开发语言·数据结构·c++·学习
H Journey2 小时前
C++ 11 新特性 基于范围的for循环
c++·c++11·for循环
无限进步_2 小时前
【C++】反转字符串的进阶技巧:每隔k个字符反转k个
java·开发语言·c++·git·算法·github·visual studio
计算机安禾2 小时前
【数据结构与算法】第34篇:选择排序:简单选择排序与堆排序
c语言·开发语言·数据结构·c++·算法·排序算法·visual studio
初夏睡觉12 小时前
c++1.3(变量与常量,简单数学运算详解),草稿公放
开发语言·c++