在C++11之前,我们处理回调函数时常常面临一个尴尬的局面:函数指针太原始,模板又太重。想象一下,你正在设计一个事件系统,需要处理各种不同类型的回调——可能是普通函数、类成员函数、甚至是lambda表达式。传统的C++98方案会让你写出大量重复代码,或者陷入类型系统的泥潭。
我曾在2013年参与一个网络库开发,当时为了支持不同形式的回调,不得不写了十几个重载版本。直到C++11的function和bind出现,这个问题才得到优雅解决。这两个工具本质上是对可调用对象的统一抽象,让"函数"这个概念变得更加一等公民。
function的声明看起来简单:
cpp复制std::function<int(double)> func;
这表示func可以包装任何接受double参数并返回int的可调用对象。它的神奇之处在于类型擦除技术——通过模板特化和虚函数表,在运行时保持类型信息的同时,提供统一的调用接口。
实际项目中,我常用function作为回调接口:
cpp复制class Button {
public:
using Callback = std::function<void()>;
void setCallback(Callback cb) { callback_ = cb; }
void click() { if(callback_) callback_(); }
private:
Callback callback_;
};
虽然function很方便,但在性能敏感场景需要谨慎:
一个常见错误是滥用function作为类成员:
cpp复制// 不推荐:每个对象都带function成员
class Widget {
std::function<void()> handler;
};
// 推荐:需要时才传递function参数
void processWidget(Widget w, std::function<void()> handler);
bind最强大的能力是参数重排序和部分绑定。假设我们有个日志函数:
cpp复制void log(int severity, const string& msg, const string& file);
我们可以用bind创建不同变体:
cpp复制using namespace std::placeholders;
auto logError = std::bind(log, 1, _1, _2); // 固定severity=1
auto logSimple = std::bind(log, 0, _1, "default.log"); // 固定部分参数
bind处理成员函数时需要特别注意this指针:
cpp复制class Server {
public:
void start(int port);
};
Server srv;
auto starter = std::bind(&Server::start, &srv, _1);
starter(8080); // 相当于srv.start(8080)
这里容易犯的错误是绑定临时对象的成员函数:
cpp复制// 危险!临时对象会立即销毁
auto badBind = std::bind(&Server::start, Server(), 8080);
C++14后,lambda通常比bind更清晰:
cpp复制// 用bind
auto oldWay = std::bind(log, 0, _1, "app.log");
// 用lambda
auto newWay = [](const string& msg) {
log(0, msg, "app.log");
};
lambda的优势:
在性能关键路径,可以考虑模板:
cpp复制template<typename F>
void highPerfAPI(F&& callback) {
// 直接内联调用
std::forward<F>(callback)();
}
function和bind对象本身是值类型,但:
我曾遇到一个崩溃案例:
cpp复制std::function<void()> task;
// 线程1
task = []{ /*...*/ };
// 线程2
task(); // 可能访问空function
解决方案是使用shared_ptr或atomic_flag保护。
function完美适配标准算法:
cpp复制std::vector<int> data{1,2,3};
std::function<bool(int)> pred = [](int x){ return x>2; };
auto it = std::find_if(data.begin(), data.end(), pred);
但在泛型代码中,直接使用模板参数通常更高效。
当function调用出错时,gdb中可以使用:
code复制p func._M_invoker._M_pfun
查看实际调用的函数地址。对于bind对象,可以用:
code复制p bindObj._M_bound_args
查看绑定的参数。
一个典型的事件总线实现:
cpp复制class EventBus {
std::unordered_map<std::type_index,
std::vector<std::function<void(const void*)>>> handlers;
public:
template<typename Event>
void subscribe(std::function<void(const Event&)> handler) {
auto wrapper = [handler](const void* ev) {
handler(*static_cast<const Event*>(ev));
};
handlers[typeid(Event)].push_back(wrapper);
}
};
实现一个延迟任务处理器:
cpp复制class Scheduler {
std::priority_queue<
std::pair<std::chrono::time_point,
std::function<void()>>> tasks;
public:
void scheduleAfter(std::chrono::milliseconds delay,
std::function<void()> task) {
auto time = std::chrono::system_clock::now() + delay;
tasks.emplace(time, std::move(task));
}
};
替代传统的虚函数策略模式:
cpp复制class Sorter {
std::function<void(std::vector<int>&)> strategy;
public:
void setStrategy(std::function<void(std::vector<int>&)> s) {
strategy = s;
}
void sort(std::vector<int>& data) {
if(strategy) strategy(data);
}
};
大多数标准库实现对小function有优化:
测试案例:
cpp复制auto small = []{ return 42; };
std::function<int()> f = small;
assert(sizeof(small) <= sizeof(f)); // 通常成立
正确使用移动语义提升性能:
cpp复制std::function<void()> createHeavyTask() {
BigObject obj; // 大对象
return [obj=std::move(obj)]{ /*...*/ }; // 移动捕获
}
对于高频创建的function,可自定义分配器:
cpp复制template<typename T>
using MyAlloc = /* 自定义内存池 */;
std::function<int(int), MyAlloc<void>> func;
不同编译器对function的实现可能不同:
某些平台可能禁用异常:
cpp复制std::function<void()> f;
try {
f(); // 可能在不同平台表现不同
} catch(...) {
// 不是所有实现都抛出
}
利用function实现灵活的mock:
cpp复制struct Database {
std::function<std::string(int)> query;
};
TEST(DatabaseTest, Query) {
Database db;
db.query = [](int id){ return "mock_" + std::to_string(id); };
ASSERT_EQ(db.query(123), "mock_123");
}
比较不同调用方式的性能:
cpp复制void rawFunc(int);
std::function<void(int)> func = rawFunc;
// 测试直接调用
benchmark("raw", []{ rawFunc(42); });
// 测试function调用
benchmark("function", [&]{ func(42); });
C++17引入了invoke,C++20又增加了bind_front,这些新特性与function/bind的关系值得关注。在我的项目中,已经开始逐步用invoke+lambda替代部分bind场景,但function由于其类型擦除的特性,在接口设计中仍然不可替代。