适配器:函数对象适配器、取反适配器、函数指针适配器、成员函数适配器
适配器这东西,说白了就是「包装」。你有一个现成的函数或对象,但接口对不上?别急着重写,拿适配器包一层就好。我在项目里经常遇到这种情况——STL算法要求传一元谓词,可我手里只有二元函数,怎么办?适配器就是干这个的。
1. 函数对象适配器:bind1st 和 bind2nd
先看最常用的。STL里很多算法要求一元谓词,比如 find_if、count_if。但有时候你手头只有二元函数,比如 less<int>() 需要两个参数。这时候 bind1st 和 bind2nd 就派上用场了。
它们的作用很简单:把一个二元函数对象,绑定其中一个参数,变成一元函数对象。
#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」。不过要注意 bind1st 和 bind2nd 的区别,搞反了结果就完全不一样了。
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;
}
unary_function 或 binary_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_fun 和 mem_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. 知识体系总览
下面这张图把适配器的关系理清楚了。你看,所有适配器都是围绕「接口转换」这个核心展开的。
6. 避坑指南与最佳实践
我曾经在一个遗留项目里看到满屏的 bind1st、ptr_fun、mem_fun 混在一起,调试起来简直噩梦。这里分享几个经验:
- 新代码用 lambda:C++11 之后,lambda 表达式可以替代绝大多数适配器场景,代码更清晰。
- 看懂老代码:很多公司还有 C++98/03 的老项目,适配器知识是必备技能。
- 注意生命周期:
mem_fun返回的函数对象内部持有成员函数指针,确保对象在调用期间存活。 - 类型匹配:
not1要求一元谓词,not2要求二元谓词,用错了编译报错信息很长,别慌。
std::function。但无论如何,理解适配器的设计思想——「接口转换」——比记住具体 API 更重要。
适配器这套东西,说白了就是 STL 设计哲学的一个缩影:通过组合和包装,让已有的组件发挥更大的作用。你想想看,一个 bind2nd 加上 not1,就能组合出各种复杂的条件判断,这就是泛型编程的魅力。
公众号:蓝海资料掘金营,微信deep3321