深入理解C++ 中 `<functional>` 头文件
📋 文章概述
<functional> 头文件是 C++ 标准库中最强大、最灵活的部分之一。它提供了处理可调用对象(callable objects)的工具——任何可以像函数一样调用的东西。无论你使用的是 lambda 表达式、函数指针、函数对象(functor)还是成员函数,<functional> 都能帮助你无缝地存储、绑定、适配和传递它们。
在本文中,我们将通过清晰实用的示例,探索 <functional> 中的每一个重要组件。读完本文,你将掌握如何使用 std::function、std::bind、占位符、std::reference_wrapper、std::mem_fn 等工具。
1️⃣ <functional> 是什么?
核心来说,<functional> 提供以下功能:
| 组件 | 作用 |
|---|---|
多态函数包装器 std::function |
类型擦除的容器,可存储任何可调用对象 |
函数绑定器 std::bind + 占位符 |
固定参数或重新排列参数顺序 |
引用包装器 std::reference_wrapper / std::ref / std::cref |
在需要拷贝的上下文中存储引用 |
成员函数适配器 std::mem_fn |
将成员函数转换为普通函数对象 |
| 内置函数对象 | 算术运算、比较、逻辑操作(如 std::plus、std::greater、std::logical_and) |
所有这些组件都能与 <algorithm> 库和现代 C++ 编程范式无缝协作。
2️⃣ std::function — 通用可调用对象包装器
std::function 是一个类型擦除包装器,可以存储任何具有指定签名的可调用对象。它就像一个"超级函数指针"。
🔹 基本用法
#include <iostream>
#include <functional>
int add(int a, int b) { return a + b; }
struct Multiply {
int operator()(int a, int b) const { return a * b; }
};
int main() {
// 存储普通函数
std::function<int(int,int)> func = add;
std::cout << func(3, 4) << '\n'; // 输出: 7
// 存储 lambda 表达式
func = [](int a, int b) { return a - b; };
std::cout << func(10, 3) << '\n'; // 输出: 7
// 存储函数对象(functor)
Multiply mult;
func = mult;
std::cout << func(5, 6) << '\n'; // 输出: 30
return 0;
}
🔹 为什么使用 std::function?
- 类型擦除:可以在同一容器中存储不同的可调用对象(例如
std::vector<std::function<bool(int,int)>>) - 可空性:
std::function可以为空,用!func或func == nullptr判断 - 目标访问:使用
func.target<T>()获取存储的对象(高级用法)
🔹 性能提示
std::function 有一定开销(虚函数调度、可能的小对象优化)。在性能敏感路径中,考虑使用模板或原始函数指针。但对于回调、事件系统或配置场景,它非常宝贵。
🔹 示例:回调函数
#include <functional>
#include <vector>
class Button {
std::vector<std::function<void()>> clickHandlers;
public:
void onClick(std::function<void()> handler) {
clickHandlers.push_back(handler);
}
void click() {
for (auto& h : clickHandlers) h();
}
};
void playSound() { /* ... */ }
int main() {
Button btn;
btn.onClick(playSound);
btn.onClick([]{ std::cout << "Button clicked!\n"; });
btn.click(); // 调用两个处理器
}
3️⃣ std::bind — 固定和重排参数
在 lambda 表达式出现之前(C++11),std::bind 是创建适配器的主要方式。如今 lambda 通常更清晰,但 std::bind 在泛型代码或需要绑定成员函数时仍有优势。
🔹 语法
auto newCallable = std::bind(callable, arg1, arg2, ..., argN);
占位符 _1、_2…(来自 std::placeholders)表示调用绑定对象时将传入的参数。
🔹 基础示例
#include <iostream>
#include <functional>
void print(int a, int b, int c) {
std::cout << a << ", " << b << ", " << c << '\n';
}
int main() {
using namespace std::placeholders;
// 固定第一个参数为 10
auto f1 = std::bind(print, 10, _1, _2);
f1(20, 30); // 输出: 10, 20, 30
// 重排参数:第三个变第一个
auto f2 = std::bind(print, _3, _1, _2);
f2(1, 2, 3); // 输出: 3, 1, 2
// 绑定成员函数(需要对象指针)
struct Demo { void say(int x) { std::cout << x << '\n'; } };
Demo d;
auto f3 = std::bind(&Demo::say, &d, _1);
f3(42); // 输出: 42
}
🔹 何时优先使用 std::bind 而非 lambda?
- 高度泛型代码:
std::bind适用于任何可调用对象,包括成员函数指针,无需std::mem_fn - 占位符的完美转发:lambda 需要
decltype技巧 - 编译期参数重排:有时比 lambda 更简洁
但lambda 通常更易读。新代码建议优先使用 lambda,除非有特定理由。
4️⃣ 占位符 — _1, _2, _3…
占位符位于 std::placeholders 命名空间中,用于告诉 std::bind 转发哪些参数以及放在什么位置。
#include <functional>
#include <iostream>
void show(int a, int b) { std::cout << a << ' ' << b << '\n'; }
int main() {
using namespace std::placeholders;
auto f = std::bind(show, _2, _1); // 交换参数
f(100, 200); // 输出: 200 100
// 也可以忽略参数:在未使用的位置使用 _1
auto g = std::bind(show, 42, _2); // 忽略第一个传入参数
g(999, 123); // 输出: 42 123
}
5️⃣ std::ref 和 std::cref — 引用包装器
默认情况下,std::bind 和 std::function 会拷贝它们的参数。要存储或传递引用,请使用 std::ref(非常量)或 std::cref(常量)。它们创建 std::reference_wrapper<T>。
🔹 为什么需要它们?
#include <functional>
#include <iostream>
void increment(int& x) { ++x; }
int main() {
int a = 5;
auto bad = std::bind(increment, a); // 拷贝了 'a'!
bad();
std::cout << a << '\n'; // 仍是 5 - 未增加
auto good = std::bind(increment, std::ref(a));
good();
std::cout << a << '\n'; // 6 - 成功!
}
🔹 在 std::function 中使用 std::ref
std::function<void()> f = std::bind(increment, std::ref(a));
也可以直接存储引用包装器:
std::reference_wrapper<int> refA = std::ref(a);
refA.get() = 10; // 修改 a
🔹 何时使用 std::cref
当你想传递常量引用时,例如避免将大对象拷贝到 lambda 或绑定的可调用对象中:
void process(const std::string& s);
std::string huge = "...";
auto bound = std::bind(process, std::cref(huge)); // 无拷贝
6️⃣ std::mem_fn — 包装成员函数
std::mem_fn 将成员函数指针转换为可调用对象,调用时需传入对象(或指针)作为第一个参数。
🔹 语法
auto wrapper = std::mem_fn(&Class::method);
wrapper(object, args...); // 传入对象引用
wrapper(&object, args...); // 传入对象指针
🔹 示例
#include <functional>
#include <iostream>
#include <vector>
#include <algorithm>
struct Person {
std::string name;
void greet() const { std::cout << "Hello, I'm " << name << '\n'; }
void setAge(int a) { age = a; }
int age = 0;
};
int main() {
std::vector<Person> people = {{"Alice"}, {"Bob"}};
// 在每个对象上调用 const 成员函数
auto greetFn = std::mem_fn(&Person::greet);
for (Person& p : people) greetFn(p); // Alice, 然后 Bob
// 与 std::for_each 配合使用
std::for_each(people.begin(), people.end(), greetFn);
// 非 const 成员函数,使用指针
auto setAgeFn = std::mem_fn(&Person::setAge);
Person alice{"Alice"};
setAgeFn(&alice, 30); // 传入指针
setAgeFn(alice, 31); // 传入引用也可
}
🔹 与 std::bind 和 lambda 的对比
std::mem_fn比std::bind(&Person::greet, _1)更简洁- lambda 同样清晰:
[](auto& p){ p.greet(); } - 当你已有可调用对象并想传递给算法而无需额外括号时,使用
std::mem_fn
7️⃣ 内置函数对象 — 算术、比较、逻辑
<functional> 头文件提供了用于常见操作的模板化函数对象。它们在 std::transform、std::sort 等算法中非常有用,通常比手写 lambda 更容易被编译器内联优化。
🔹 算术运算
| 函数对象 | 操作 |
|---|---|
std::plus<T> |
+ |
std::minus<T> |
- |
std::multiplies<T> |
* |
std::divides<T> |
/ |
std::modulus<T> |
% |
std::negate<T> |
-(一元) |
🔹 比较运算
| 函数对象 | 操作 |
|---|---|
std::equal_to<T> |
== |
std::not_equal_to<T> |
!= |
std::greater<T> |
> |
std::less<T> |
< |
std::greater_equal<T> |
>= |
std::less_equal<T> |
<= |
🔹 逻辑运算
| 函数对象 | 操作 |
|---|---|
std::logical_and<T> |
&& |
std::logical_or<T> |
` |
std::logical_not<T> |
! |
🔹 示例
#include <algorithm>
#include <functional>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5};
// 降序排序
std::sort(v.begin(), v.end(), std::greater<int>());
// v = {5,4,3,1,1}
// 变换:每个元素乘以 2
std::vector<int> result;
std::transform(v.begin(), v.end(), std::back_inserter(result),
std::bind(std::multiplies<int>(), std::placeholders::_1, 2));
// result = {10,8,6,2,2}
// 使用 plus 进行累加
int sum = std::accumulate(v.begin(), v.end(), 0, std::plus<int>());
std::cout << sum << '\n'; // 14
}
8️⃣ 高级话题:std::function 与仅移动类型
std::function 要求存储的可调用对象必须是可拷贝构造的。对于仅移动类型(例如捕获了 std::unique_ptr 的 lambda),你需要其他方案——如 C++23 的 std::move_only_function 或自定义包装器。在 C++14/17 中,可使用 std::packaged_task 或设计自己的类型擦除接口。
// C++23 示例(尚未广泛支持)
#include <functional>
std::move_only_function<void()> f = [ptr = std::make_unique<int>(42)] { /*...*/ };
9️⃣ 性能与最佳实践
🔹 何时使用哪种工具
| 工具 | 适用场景 |
|---|---|
| 原始函数指针 | 极轻量、无捕获、固定签名 |
| Lambda(auto) | 大多数局部使用,最佳优化效果 |
std::function |
存储异构可调用对象、回调、需要类型擦除 |
std::bind |
遗留代码或高级完美转发场景 |
std::mem_fn |
需要为成员函数创建可调用对象,尤其在泛型算法中 |
std::ref / std::cref |
任何想避免拷贝到 bind/function 的场景 |
| 内置函数对象 | 标准操作,通常比 lambda 更快(编译器更了解它们) |
🔹 避免不必要的 std::function
// ❌ 不好(无谓的开销)
void forEach(const std::vector<int>& v, std::function<void(int)> f) { ... }
// ✅ 好(模板,零开销)
template<typename F>
void forEach(const std::vector<int>& v, F f) { ... }
🔹 小对象优化(Small-Object Optimisation)
std::function 通常会内联存储小型可调用对象(例如无捕获的 lambda),避免堆分配。对于较大对象,则会在堆上分配。
🔟 完整实战示例 — 事件系统
让我们将所有内容整合到一个小型事件调度器中:
#include <functional>
#include <vector>
#include <map>
#include <iostream>
class EventDispatcher {
using Handler = std::function<void(const std::string&, int)>;
std::multimap<std::string, Handler> handlers;
public:
void subscribe(const std::string& event, Handler h) {
handlers.emplace(event, std::move(h));
}
void emit(const std::string& event, int value) {
auto range = handlers.equal_range(event);
for (auto it = range.first; it != range.second; ++it) {
it->second(event, value);
}
}
};
struct Logger {
void log(const std::string& ev, int val) const {
std::cout << "[Logger] " << ev << " -> " << val << '\n';
}
};
int main() {
EventDispatcher dispatcher;
// Lambda 处理器
dispatcher.subscribe("click", [](const std::string& e, int v) {
std::cout << "Lambda: " << e << " with " << v << '\n';
});
// 普通函数处理器
auto freeHandler = [](const std::string& e, int v) {
std::cout << "Free: " << e << " = " << v << '\n';
};
dispatcher.subscribe("update", freeHandler);
// 使用 bind + ref 的成员函数处理器
Logger logger;
dispatcher.subscribe("log", std::bind(&Logger::log, &logger,
std::placeholders::_1, std::placeholders::_2));
// 触发事件
dispatcher.emit("click", 42);
dispatcher.emit("update", 100);
dispatcher.emit("log", 99);
}
输出:
Lambda: click with 42
Free: update = 100
[Logger] log -> 99
🎯 总结
#include <functional> 是现代 C++ 开发者不可或缺的工具。它提供了:
- ✅
std::function:用于类型擦除的可调用对象 - ✅
std::bind+ 占位符:用于参数适配 - ✅
std::ref/std::cref:用于引用语义 - ✅
std::mem_fn:用于成员函数指针 - ✅ 便捷的函数对象:用于标准操作
虽然 lambda 表达式已经取代了 std::bind 的许多用途,但 <functional> 头文件对于编写灵活、泛型且表达力强的 C++ 代码仍然不可或缺。明智地使用它,你的回调、算法和事件系统将变得既强大又清晰。