适配器:函数对象适配器、取反适配器、函数指针适配器、成员函数适配器

适配器这东西,说白了就是「包装」。你有一个现成的函数或对象,但接口对不上?别急着重写,拿适配器包一层就好。我在项目里经常遇到这种情况——STL算法要求传一元谓词,可我手里只有二元函数,怎么办?适配器就是干这个的。

1. 函数对象适配器:bind1st 和 bind2nd

先看最常用的。STL里很多算法要求一元谓词,比如 find_ifcount_if。但有时候你手头只有二元函数,比如 less<int>() 需要两个参数。这时候 bind1stbind2nd 就派上用场了。

它们的作用很简单:把一个二元函数对象,绑定其中一个参数,变成一元函数对象。

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

int main() {
    std::vector<int> vec = {10, 20, 30, 40, 50, 60, 70};

    // 找出第一个大于 40 的元素
    auto it = std::find_if(vec.begin(), vec.end(),
                           std::bind2nd(std::greater<int>(), 40));

    if (it != vec.end()) {
        std::cout << "第一个大于40的元素是: " << *it << std::endl;
    }

    // 统计小于 35 的元素个数
    int count = std::count_if(vec.begin(), vec.end(),
                              std::bind1st(std::greater<int>(), 35));
    // 注意:bind1st(greater, 35) 相当于 35 > x,即 x < 35
    std::cout << "小于35的元素个数: " << count << std::endl;

    return 0;
}

我个人习惯用 bind2nd 更多一些,因为语义更直观——bind2nd(greater<int>(), 40) 就是「大于40」。不过要注意 bind1stbind2nd 的区别,搞反了结果就完全不一样了。

注意:C++11 之后,bind1st 和 bind2nd 已被标记为 deprecated。新项目建议用 std::bind 或 lambda 表达式。但老代码里大量存在,看懂它们依然很重要。

2. 取反适配器:not1 和 not2

取反适配器,顾名思义,就是把谓词的结果反过来。not1 用于一元谓词,not2 用于二元谓词。

我在项目中遇到过这样一个场景:要找出容器里所有「不满足某个条件」的元素。如果直接写 lambda 当然可以,但用 not1 配合已有谓词,代码更简洁,也更容易维护。

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

bool isEven(int n) {
    return n % 2 == 0;
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 找出第一个奇数(对 isEven 取反)
    auto it = std::find_if(vec.begin(), vec.end(),
                           std::not1(std::ptr_fun(isEven)));

    if (it != vec.end()) {
        std::cout << "第一个奇数是: " << *it << std::endl;
    }

    // 使用 not2 对二元谓词取反
    // 找出第一个不小于 5 的元素(即大于等于 5)
    auto it2 = std::find_if(vec.begin(), vec.end(),
                            std::not2(std::bind2nd(std::less<int>(), 5)));

    if (it2 != vec.end()) {
        std::cout << "第一个不小于5的元素是: " << *it2 << std::endl;
    }

    return 0;
}
小技巧:not1 和 not2 要求传入的谓词必须继承自 unary_functionbinary_function。普通函数指针需要用 ptr_fun 包装一下才能用。

3. 函数指针适配器:ptr_fun

普通函数指针不能直接用于 STL 算法,因为 STL 期望的是函数对象(functor)。ptr_fun 的作用就是把普通函数指针包装成函数对象。

嗯,这里要注意:ptr_fun 在 C++11 之后也被标记为 deprecated 了。但老项目里遍地都是,你总得看得懂。

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

bool isOdd(int n) {
    return n % 2 != 0;
}

int main() {
    std::vector<int> vec = {2, 4, 6, 7, 8, 10};

    // 用 ptr_fun 包装普通函数
    auto it = std::find_if(vec.begin(), vec.end(),
                           std::ptr_fun(isOdd));

    if (it != vec.end()) {
        std::cout << "第一个奇数是: " << *it << std::endl;
    }

    // 配合 not1 使用
    auto it2 = std::find_if(vec.begin(), vec.end(),
                            std::not1(std::ptr_fun(isOdd)));

    if (it2 != vec.end()) {
        std::cout << "第一个偶数是: " << *it2 << std::endl;
    }

    return 0;
}

为什么需要 ptr_fun?因为 STL 算法内部会拷贝谓词对象,而函数指针的拷贝语义和函数对象不同。ptr_fun 提供了必要的类型信息,让算法能正确使用它。

4. 成员函数适配器:mem_fun 和 mem_fun_ref

这个我特别喜欢。你想对容器里的对象调用某个成员函数,但算法只接受普通函数或函数对象,怎么办?mem_funmem_fun_ref 就是干这个的。

区别很简单:mem_fun 用于指针容器(如 vector<MyClass*>),mem_fun_ref 用于对象容器(如 vector<MyClass>)。

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

class Employee {
public:
    Employee(const std::string& name, int salary)
        : name_(name), salary_(salary) {}

    void print() const {
        std::cout << name_ << ": " << salary_ << std::endl;
    }

    bool isHighSalary() const {
        return salary_ > 5000;
    }

private:
    std::string name_;
    int salary_;
};

int main() {
    // 对象容器,使用 mem_fun_ref
    std::vector<Employee> employees = {
        Employee("张三", 3000),
        Employee("李四", 8000),
        Employee("王五", 4500)
    };

    std::cout << "所有员工信息:" << std::endl;
    std::for_each(employees.begin(), employees.end(),
                  std::mem_fun_ref(&Employee::print));

    // 指针容器,使用 mem_fun
    std::vector<Employee*> empPtrs;
    empPtrs.push_back(new Employee("赵六", 6000));
    empPtrs.push_back(new Employee("钱七", 2000));

    std::cout << "\n指针容器中的高薪员工:" << std::endl;
    auto it = std::find_if(empPtrs.begin(), empPtrs.end(),
                           std::mem_fun(&Employee::isHighSalary));
    if (it != empPtrs.end()) {
        (*it)->print();
    }

    // 别忘了释放内存
    for (auto p : empPtrs) delete p;

    return 0;
}
核心要点:
  • mem_fun → 容器元素是指针时用
  • mem_fun_ref → 容器元素是对象时用
  • 成员函数必须用 &类名::函数名 的形式取地址

5. 知识体系总览

下面这张图把适配器的关系理清楚了。你看,所有适配器都是围绕「接口转换」这个核心展开的。

STL 适配器体系 适配器 (Adapter) 函数对象适配器 bind1st / bind2nd 取反适配器 not1 / not2 函数指针适配器 ptr_fun 成员函数适配器 mem_fun / mem_fun_ref 核心思想:将不兼容的接口包装成 STL 算法可用的形式

6. 避坑指南与最佳实践

我曾经在一个遗留项目里看到满屏的 bind1stptr_funmem_fun 混在一起,调试起来简直噩梦。这里分享几个经验:

  • 新代码用 lambda:C++11 之后,lambda 表达式可以替代绝大多数适配器场景,代码更清晰。
  • 看懂老代码:很多公司还有 C++98/03 的老项目,适配器知识是必备技能。
  • 注意生命周期mem_fun 返回的函数对象内部持有成员函数指针,确保对象在调用期间存活。
  • 类型匹配not1 要求一元谓词,not2 要求二元谓词,用错了编译报错信息很长,别慌。
我的建议:如果你在维护老代码,把适配器用法记牢。如果写新项目,优先用 lambda + std::function。但无论如何,理解适配器的设计思想——「接口转换」——比记住具体 API 更重要。

适配器这套东西,说白了就是 STL 设计哲学的一个缩影:通过组合和包装,让已有的组件发挥更大的作用。你想想看,一个 bind2nd 加上 not1,就能组合出各种复杂的条件判断,这就是泛型编程的魅力。


公众号:蓝海资料掘金营,微信deep3321