函数对象与 std::function
定义与作用
函数对象(Function Object / Functor)是重载了 operator() 的类的实例,可以像函数一样被"调用"。与普通函数相比,函数对象可以携带状态:
class Adder {
int base_;
public:
Adder(int base) : base_(base) {}
int operator()(int x) const { return base_ + x; }
};
Adder add5(5);
add5(10); // 返回 15——带状态的计算
std::function(C++11)是类型擦除的通用可调用对象包装器,可以统一存储:普通函数、Lambda、函数对象、成员函数指针、std::bind 结果等。
| 机制 | 特点 | 何时使用 |
|---|---|---|
| 函数对象(Functor) | 编译期确定类型,零开销 | 状态简单的局部回调 |
| Lambda | 匿名 Functor,就地定义 | 短小的一次性回调 |
std::function | 类型擦除,有运行时开销 | 需要存储/传递不同类型可调用对象 |
std::bind | 参数绑定 | 适配函数签名(C++14 起推荐 Lambda 替代) |
核心原理
std::function 的类型擦除机制
可调用对象体系
完整示例
示例一:函数对象实现灵活的比较策略
场景说明:学生成绩系统使用函数对象封装可配置的比较策略。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
struct Student {
std::string name;
int id;
double gpa;
};
// 按 GPA 比较(降序)
struct CompareByGPA {
explicit CompareByGPA(bool ascending = false)
: ascending_(ascending) {}
bool operator()(const Student& a, const Student& b) const {
return ascending_ ? a.gpa < b.gpa : a.gpa > b.gpa;
}
private:
bool ascending_; // 状态:升序还是降序
};
// 按 ID 比较
struct CompareByID {
bool operator()(const Student& a, const Student& b) const {
return a.id < b.id;
}
};
// 通用排序函数:通过模板参数接收任何可调用对象
template <typename Comparator>
void sort_students(std::vector<Student>& students, Comparator cmp) {
std::sort(students.begin(), students.end(), cmp);
}
// 使用 std::function 的版本:运行时选择比较器
void sort_students_runtime(std::vector<Student>& students,
std::function<bool(const Student&, const Student&)> cmp) {
std::sort(students.begin(), students.end(), cmp);
}
int main() {
std::vector<Student> students = {
{"张三", 1003, 3.8},
{"李四", 1001, 3.5},
{"王五", 1002, 4.0}
};
// ---- 模板版本:编译期多态,零开销 ----
std::cout << "===== 按 GPA 降序(函数对象) =====" << std::endl;
sort_students(students, CompareByGPA(false));
for (const auto& s : students) {
std::cout << s.name << " | GPA " << s.gpa << std::endl;
}
std::cout << "\n===== 按 GPA 升序 =====" << std::endl;
sort_students(students, CompareByGPA(true));
for (const auto& s : students) {
std::cout << s.name << " | GPA " << s.gpa << std::endl;
}
// ---- std::function 版本:运行期多态 ----
std::cout << "\n===== std::function 统一接口 =====" << std::endl;
std::function<bool(const Student&, const Student&)> comparator;
// 可以存 Lambda
comparator = [](const Student& a, const Student& b) {
return a.id < b.id;
};
sort_students_runtime(students, comparator);
std::cout << "按 ID: ";
for (const auto& s : students) std::cout << s.id << " ";
std::cout << std::endl;
// 可以存函数对象
comparator = CompareByGPA(true);
sort_students_runtime(students, comparator);
std::cout << "按 GPA 升序: ";
for (const auto& s : students) std::cout << s.name << " ";
std::cout << std::endl;
// 查询 std::function 是否有效
std::function<void()> empty_func;
if (!empty_func) {
std::cout << "\nstd::function 可以为空(类似空指针)" << std::endl;
}
return 0;
}
预期输出:
===== 按 GPA 降序(函数对象) =====
王五 | GPA 4
张三 | GPA 3.8
李四 | GPA 3.5
===== 按 GPA 升序 =====
李四 | GPA 3.5
张三 | GPA 3.8
王五 | GPA 4
===== std::function 统一接口 =====
按 ID: 1001 1002 1003
按 GPA 升序: 李四 张三 王五
std::function 可以为空(类似空指针)
逐段分析:
CompareByGPA是典型的带状态函数对象——构造时决定升降序,operator()中使用该状态CompareByID是无状态函数对象——所有实例行为相同- 模板版本
sort_students是编译期多态:每个Comparator类型生成单独代码,零运行时开销 std::function版本是运行期多态:同一变量可换绑不同可调用对象,但有间接调用开销std::function可以像空指针一样处于"无效"状态,if (func)判断是否有效
示例二:std::bind 与回调注册系统
场景说明:事件系统用 std::bind 适配函数签名,注册各种格式的回调。
#include <iostream>
#include <functional>
#include <vector>
#include <string>
class EventManager {
public:
using Callback = std::function<void()>;
void on(const std::string& event, Callback cb) {
callbacks_[event].push_back(std::move(cb));
}
void trigger(const std::string& event) {
auto it = callbacks_.find(event);
if (it != callbacks_.end()) {
for (auto& cb : it->second) {
cb();
}
}
}
private:
std::map<std::string, std::vector<Callback>> callbacks_;
};
// 不同签名的处理函数
void free_handler() {
std::cout << " [普通函数] 事件已处理" << std::endl;
}
void param_handler(int id, const std::string& msg) {
std::cout << " [参数函数] ID=" << id << ", MSG=" << msg << std::endl;
}
class SystemMonitor {
public:
void on_startup(int code) {
std::cout << " [成员函数] 启动码: " << code << std::endl;
}
};
int main() {
EventManager em;
SystemMonitor monitor;
// 注册方式 1:Lambda(最灵活)
em.on("startup", []() {
std::cout << " [Lambda] 系统初始化..." << std::endl;
});
// 注册方式 2:普通函数
em.on("startup", free_handler);
// 注册方式 3:用 std::bind 适配参数函数
// param_handler 有参数,但 Callback 签名是 void()
em.on("startup", std::bind(param_handler, 100, "数据库就绪"));
em.on("startup", std::bind(param_handler, 200, "网络就绪"));
// 注册方式 4:用 std::bind 绑定成员函数
// monitor.on_startup(int) → void()
em.on("startup", std::bind(&SystemMonitor::on_startup, &monitor, 0));
// Lambda 替代 bind(更清晰的写法)
em.on("startup", [&monitor]() {
monitor.on_startup(999);
});
std::cout << "===== 触发 startup 事件 =====" << std::endl;
em.trigger("startup");
return 0;
}
预期输出:
===== 触发 startup 事件 =====
[Lambda] 系统初始化...
[普通函数] 事件已处理
[参数函数] ID=100, MSG=数据库就绪
[参数函数] ID=200, MSG=网络就绪
[成员函数] 启动码: 0
[成员函数] 启动码: 999
逐段分析:
EventManager::Callback是std::function<void()>——统一的无参回调接口std::bind(param_handler, 100, "数据库就绪")将两参函数适配为无参可调用对象std::bind(&SystemMonitor::on_startup, &monitor, 0)绑定成员函数——需要传入对象指针- Lambda 写法
[&monitor]() { monitor.on_startup(999); }语义比std::bind更清晰——C++14 起推荐使用 Lambda 替代std::bind - 每次注册不同类型的可调用对象,
std::function通过类型擦除将它们统一存储
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
std::bind 参数按值传递 | 大对象被拷贝 | 用 std::ref 或 std::cref 包装引用 |
std::bind 中占位符顺序混乱 | 参数位置错误 | 仔细核对 _1、_2 的顺序 |
std::function 为空时调用 | std::bad_function_call 异常 | 调用前检查 if (func) |
std::function 频繁拷贝 | 性能开销 | 使用模板 + auto 替代 |
用 std::bind 绑定重载函数 | 歧义,编译失败 | 用 static_cast 明确签名或 Lambda |
常见面试问题
什么是函数对象(Functor)?与普通函数的区别?——重载
operator()的类。区别在于函数对象可以携带状态(成员变量),且是类型而非函数指针。std::function的实现原理?它为什么有开销?——类型擦除:通过虚函数表或函数指针包装任意可调用对象。开销来自间接调用和可能的堆分配(小对象优化 SBO 可缓解)。std::bind和 Lambda 哪个更好?为什么?——C++14 起推荐 Lambda。Lambda 语法更清晰,编译器优化更容易(可见的调用),std::bind的参数求值时机和传参方式容易造成困惑。如何用
std::function存储成员函数?——std::function<void(MyClass*, int)> f = &MyClass::method;调用时f(&obj, 42)。或者用std::bind预先绑定对象。std::function能否存储有捕获的 Lambda?——可以。std::function通过类型擦除支持任意可调用对象,包括有捕获 Lambda。
小结
- 函数对象 = 重载
operator()的类,可携带状态,模板配合零开销 std::function= 类型擦除的统一可调用对象包装器,灵活但有运行时开销std::bind用于适配函数签名,但 C++14 起优先用 Lambda 替代- 性能敏感路径用模板(编译期多态),灵活性需求用
std::function(运行期多态) std::function可处于空状态,调用前检查