std::function 是 C++11 中引入的一个极其有用的工具,位于 <functional> 头文件中。它是一个通用的、多态的函数包装器。它的实例可以存储、复制和调用任何可调用 (Callable) 目标——包括普通函数、Lambda 表达式、函数指针、成员函数指针、以及函数对象(functors)。


1. std::function 是什么?

一句话定义

std::function 是一个类型安全的包装器,它可以持有任何符合其函数签名的可调用对象。

可以把它想象成一个“万能的函数指针”,但它比函数指针强大得多,因为它可以指向任何可调用的东西,而不仅仅是全局函数。

解决的问题:类型不统一的“可调用物”

在 C++ 中,有很多东西都可以被“调用”,比如:

  • 普通函数指针: void (*p_func)(int);
  • 函数对象 (Functor): 一个重载了 operator() 的类的对象。每个函数对象都有自己独特的类型。
  • Lambda 表达式: 编译器会为每个 Lambda 生成一个唯一的、匿名的闭包类型。

std::function 出现之前,如果你想编写一个函数来接受一个“回调”,你将面临一个难题。例如,一个按钮的点击事件处理:

1
2
3
4
// 我想让这个函数能接受任何“无参数、无返回值”的回调
void set_on_click_handler(??? handler) {
// ...
}

这里的 ??? 应该是什么类型?

  • 如果是 void (*)(),那么它就不能接受带捕获的 Lambda 或函数对象。
  • 如果使用模板 template <typename T> void set_on_click_handler(T handler),那么这个类型会“渗透”到你的类定义中,使其也必须是模板,这会大大增加复杂性。

std::function 完美地解决了这个问题。它提供了一个统一的类型来表示所有具有相同签名的可调用对象。

1
2
3
4
#include <functional>
void set_on_click_handler(std::function<void()> handler) {
// ...
}

现在,任何无参数、无返回值的可调用物都可以传递给这个函数了。


2. 基本语法和用法

包含头文件

1
#include <functional>

声明与初始化

std::function 是一个类模板,其模板参数是函数签名

1
2
3
4
5
6
// 声明一个可以持有“接受一个 int 和一个 double,返回一个 bool”的函数的包装器
std::function<bool(int, double)> my_func;

// 声明并初始化
double my_division(int a, int b) { return static_cast<double>(a) / b; }
std::function<double(int, int)> func_divider = my_division;

调用

调用 std::function 对象就像调用一个普通函数一样。

1
double result = func_divider(10, 3); // result is 3.333...

检查是否为空

一个未初始化的 std::function 对象是“空的”。在调用之前检查它是否持有可调用对象是一个好习惯,否则会抛出 std::bad_function_call 异常。

1
2
3
4
5
6
7
8
9
10
11
12
std::function<void()> empty_func;

if (empty_func) { // bool conversion operator checks if it's non-empty
empty_func(); // 如果不检查,这行会抛出异常
} else {
std::cout << "Function wrapper is empty." << std::endl;
}

// 也可以与 nullptr 比较
if (empty_func == nullptr) {
std::cout << "It's null!" << std::endl;
}

3. std::function 可以存储什么?

这是 std::function 最强大的地方。只要签名匹配,它可以存储几乎任何可调用物。

示例1:普通函数 (Free Functions)

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <functional>

void print_message(const std::string& msg) {
std::cout << "Message: " << msg << std::endl;
}

int main() {
std::function<void(const std::string&)> func = print_message;
func("Hello from a free function!");
}

示例2:Lambda 表达式

这是最常见的用法,尤其是对于带状态(有捕获)的 Lambda。

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
int counter = 10;

// 一个捕获了 counter 变量的 Lambda
std::function<void()> lambda_func = [&counter]() {
counter++;
std::cout << "Counter value: " << counter << std::endl;
};

lambda_func(); // 输出: Counter value: 11
lambda_func(); // 输出: Counter value: 12
std::cout << "Original counter: " << counter << std::endl; // 输出: Original counter: 12
}

示例3:函数对象 (Functors)

一个重载了 operator() 的类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Multiplier {
int factor;
Multiplier(int f) : factor(f) {}

int operator()(int value) const {
return value * factor;
}
};

int main() {
Multiplier mul_by_5(5);

// Functor 对象可以直接赋值给 std::function
std::function<int(int)> func = mul_by_5;

std::cout << "5 * 10 = " << func(10) << std::endl; // 输出: 5 * 10 = 50

// 也可以用临时对象或 Lambda 创建
std::function<int(int)> func2 = Multiplier{100};
std::cout << "100 * 10 = " << func2(10) << std::endl; // 输出: 100 * 10 = 1000
}

示例4:类的成员函数

这是稍微复杂一点的情况,因为成员函数需要一个 this 指针(即一个类的实例)来调用。通常有两种方法来包装它们。

方法 A:使用 std::bind (传统方式)

std::bind 可以将成员函数、对象实例和参数“绑定”在一起,生成一个新的可调用对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <functional>
#include <string>

using namespace std::placeholders; // for _1, _2, etc.

struct Greeter {
void say_hello(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
};

int main() {
Greeter greeter_obj;

// 绑定 Greeter::say_hello 成员函数到 greeter_obj 实例上
// _1 是一个占位符,表示 std::function 调用时的第一个参数
std::function<void(const std::string&)> member_func =
std::bind(&Greeter::say_hello, &greeter_obj, _1);

member_func("World"); // 实际上调用的是 greeter_obj.say_hello("World")
}

方法 B:使用 Lambda 表达式 (现代 C++ 推荐方式)

Lambda 表达式通常更清晰、更易读。

1
2
3
4
5
6
7
8
9
10
11
int main() {
Greeter greeter_obj;

// 用 Lambda 捕获对象实例的引用(或指针)
std::function<void(const std::string&)> member_func_lambda =
[&greeter_obj](const std::string& name) {
greeter_obj.say_hello(name);
};

member_func_lambda("Alice"); // 调用 Lambda,Lambda 内部再调用成员函数
}

4. std::function 的内部机制与关键特性

类型擦除 (Type Erasure)

std::function 的核心魔法是类型擦除。当你把一个 Lambda (类型为 ClosureType_A)或者一个 Functor (类型为 MyFunctor)赋值给 std::function<void()> 时,std::function 内部会通过动态内存(通常)和虚函数来“擦除”原始对象的具体类型,只保留其“可调用性”和函数签名。

它内部大致像这样(极度简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 伪代码
class function {
// 指向一个抽象基类的指针
CallableBase* callable_ptr;
public:
template<typename F>
function(F f) {
// 为具体的 f 类型创建一个派生类实例,并存入基类指针
callable_ptr = new ConcreteCallable<F>(f);
}

R operator()(Args... args) {
// 通过虚函数调用
return callable_ptr->invoke(args...);
}
};

这就是为什么它可以接受不同类型的可调用对象——它们都被包装在了一个统一的接口后面。

小对象优化 (Small Object Optimization, SOO)

由于类型擦除通常需要堆分配(new),这会带来性能开销。为了缓解这个问题,大多数标准库的 std::function 实现都采用了小对象优化

如果赋给 std::function 的可调用对象足够小(比如一个不捕获任何东西的 Lambda 或一个小的函数对象),它会被直接存储在 std::function 对象自身的内存缓冲区中,从而避免了堆分配。这大大提高了在这些常见情况下的性能。

性能考量

  • 调用开销: 调用 std::function 通常比直接调用函数或通过函数指针调用要慢,因为它可能涉及一次虚函数分派。
  • 构造/赋值开销: 如果没有触发小对象优化,构造或赋值 std::function 会有一次堆内存分配的开销。
  • 内存占用: std::function 对象本身的大小通常是几个指针的大小,用于存储内部缓冲区和管理信息。

5. 实际应用场景

场景1:实现回调函数系统

这是 std::function 的经典用途。比如一个定时器类,它在特定时间后执行一个回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Timer {
public:
void start(int milliseconds, std::function<void()> on_timeout) {
// 存储回调
this->callback_ = on_timeout;
// 启动一个线程或系统定时器...
// ...在时间到了之后...
if (this->callback_) {
this->callback_();
}
}
private:
std::function<void()> callback_;
};

场景2:作为策略模式的轻量级替代方案

在策略模式中,我们通常定义一个接口和多个实现类。如果策略很简单,std::function 可以提供一种更轻量级的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Context
class TextFormatter {
public:
using FormattingStrategy = std::function<std::string(const std::string&)>;

void setStrategy(FormattingStrategy strategy) {
strategy_ = std::move(strategy);
}

void print(const std::string& text) {
if (strategy_) {
std::cout << strategy_(text) << std::endl;
}
}
private:
FormattingStrategy strategy_;
};

int main() {
TextFormatter formatter;

formatter.setStrategy([](const std::string& s) { return "### " + s + " ###"; });
formatter.print("Title"); // 输出: ### Title ###

formatter.setStrategy([](const std::string& s) { return "*** " + s + " ***"; });
formatter.print("Important"); // 输出: *** Important ***
}

场景3:在容器中存储不同的操作

比如,构建一个命令分派器。

1
2
3
4
5
6
7
8
9
10
11
12
std::map<std::string, std::function<void()>> command_map;

void command_save() { std::cout << "Saving file..." << std::endl; }
void command_exit() { std::cout << "Exiting application..." << std::endl; }

command_map["save"] = command_save;
command_map["exit"] = command_exit;

std::string user_input = "save";
if (command_map.count(user_input)) {
command_map[user_input](); // 调用 "save" 对应的函数
}

6. 注意事项与最佳实践

std::function 的调用

如前所述,调用一个空的 std::function 会抛出 std::bad_function_call 异常。在调用前务必检查

捕获的生命周期问题

当 Lambda 捕获变量时(尤其是按引用捕获 [&]),你需要非常小心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::function<void()> create_bad_callback() {
int local_value = 42;
// 错误!捕获了局部变量的引用
return [&]() {
// 当这个 Lambda 被调用时,local_value 已经销毁了!
// 这会导致未定义行为(通常是程序崩溃)。
std::cout << "Value: " << local_value << std::endl;
};
}

int main() {
std::function<void()> bad_func = create_bad_callback();
bad_func(); // 危险操作!
}

规则: 确保被 Lambda 捕获的对象的生命周期不短于 std::function 对象本身及其任何副本的生命周期。对于局部变量,要么按值捕获 [=],要么使用 std::shared_ptr 等智能指针来管理生命周期。

与模板的对比:何时使用 std::function

  • 使用模板: 当你可以在编译时确定可调用对象的类型,并且不需要在运行时更改它时,优先使用模板。模板会为每种类型生成专用代码,编译器可以进行内联等优化,性能最高(通常是零开销)。

    1
    2
    3
    4
    template <typename Callable>
    void run_task(Callable task) {
    task(); // 编译器可以直接内联
    }
  • 使用 std::function: 当你需要在运行时存储或传递一个类型未知的可调用对象时,使用 std::function。这是典型的“运行时多态”场景,比如存储回调的容器、需要动态切换的策略等。你为此付出的代价是潜在的性能开销。


7. 总结

std::function 是现代 C++ 中一个强大而灵活的组件。它通过类型擦除技术,为 C++ 中各种形式的“可调用物”提供了一个统一的、类型安全的接口。

要点回顾:

  • 它是通用的函数包装器
  • 它能存储函数、Lambda、Functor、成员函数等。
  • 它是实现回调、事件处理和策略模式的利器。
  • 使用它需要注意生命周期性能开销
  • 在性能敏感且类型可在编译期确定的场景下,模板是更好的选择