在 C++11 里,std::function 和 std::bind 让处理各种可调用对象变得简单多了。以前函数指针、仿函数、成员函数指针一大堆写法,光记语法就够头疼。现在有了这俩工具,写回调、保存函数、延迟执行都顺手不少。

可调用对象

什么叫“可调用对象”?

C++ 里,能像函数那样用括号调用的东西都算:普通函数指针、仿函数(重载了 operator() 的类)、能转成函数指针的对象、成员函数指针、甚至成员变量指针。

1
2
3
void func(int x) {
cout << "func(int x): " << x << endl;
}

这段代码定义了一个最基础的普通函数 func,它接受一个 int 类型的参数并打印输出,是可调用对象中最简单的形式,后续会通过函数指针的方式来调用它。

1
2
3
4
5
struct Foo{
void operator()(int x){
cout << "Foo::operator(int x): " << x << endl;
}
};

这个 Foo 结构体是一个典型的仿函数,它重载了 operator() 运算符,使得 Foo 的对象可以像普通函数一样,通过 “对象名(参数)” 的形式被调用,体现了类对象作为可调用对象的用法。

1
2
3
4
5
6
7
8
9
struct Bar{
typedef void (*pFun)(int);
static void func(int x){
cout << "Bar::func(int x): " << x << endl;
}
operator pFun() const{
return func;
}
};

Bar 结构体展示了 “可被转换为函数指针的类对象” 这一可调用类型:它内部定义了静态成员函数 func,同时通过 operator pFun() 提供了向函数指针类型转换的能力,因此 Bar 的对象可以直接像函数指针一样被调用。

1
2
3
4
5
6
7
struct Test{
int a_;
Test(int x = 0) : a_(x) {}
void mem_func(int x){
cout << "Test::mem_func(int x): x=" << x << " a_=" << a_ << endl;
}
};

Test 结构体用于演示类成员相关的可调用对象,它包含一个成员变量 a_ 和一个成员函数 mem_func,后续会通过类成员函数指针和类成员指针的特殊语法,来调用这个类的成员函数和操作成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){
int x = 10;
void (*func_ptr)(int) = func;// 函数指针
func_ptr(x);
Foo foo;// 仿函数
foo(x);
Bar bar;// 可被转换为函数指针的类对象
bar(x);
void (Test:: * mem_func_ptr)(int) = &Test::mem_func;// 类成员函数指针
int Test::* mem_val_ptr = &Test::a_;// 类成员指针
Test test;
(test.*mem_func_ptr)(x);
test.*mem_val_ptr = 123;
cout << test.a_ << endl;
return 0;
}

main 函数集中展示了所有可调用对象的调用方式:先定义函数指针指向 func 并调用,接着创建 Foo 仿函数对象调用,再通过 Bar 对象的类型转换特性调用,最后定义类成员函数指针和成员指针,用 Test 对象通过特殊语法调用成员函数、修改成员变量。

除了成员指针需要特殊语法,其他的都能直接“对象+括号”调用。问题是,写法太杂,想统一保存和传递这些可调用对象很麻烦。std::function 就是为了解决这个头疼事。

内存对齐

在内存对齐的规则体系中,偏移量是理解结构体内存布局的核心。它指的是结构体中每个成员的起始地址相对于整个结构体对象起始地址的字节数,结构体第一个成员的偏移量固定为 0,后续所有成员的偏移量计算都必须遵循 “对齐规则”:每个成员的起始偏移量,必须是该成员自身数据类型大小与编译器默认对齐数(通常为 8 字节)中较小值的整数倍,若当前偏移量不满足这个要求,就需要在该成员之前填充若干字节的 “内存空洞”,直到偏移量符合规则为止。

1
2
3
4
5
6
7
8
9
10
11
struct T {
int sum = 0;
double avg = 12.23;
int count = 1;
char ch = 'a';
public:
T(int x) : sum(x), avg(x * 2.0), count(x), ch('a' + x) {}
void mem_func() {
cout << "sum: " << sum << ", avg: " << avg << ", count: " << count << ", ch: " << ch << endl;
}
};

如 Test 结构体所示,第一个成员 sum 是 int 类型(4 字节),它的偏移量为 0,0 是 4 的整数倍,完全符合对齐规则,因此 sum 占用 0 到 3 字节的内存空间。接下来要存放的是 double 类型的 dx(8 字节),此时下一个可用的偏移量是 4,但 4 并不是 8 的整数倍(dx 自身大小为 8,默认对齐数 8,取较小值仍为 8),所以需要在 sum 之后填充 4 个字节的内存空洞,让 dx 的起始偏移量调整到 8,满足对齐要求,因此 dx 占用 8 到 15 字节。dx 之后的 num 是 int 类型(4 字节),此时可用偏移量是 16,16 是 4 的整数倍,无需填充,num 占用 16 到 19 字节。再往后的 ch 是 char 类型(1 字节),可用偏移量是 20,20 是 1 的整数倍,直接存放,ch 占用 20 字节这一个位置。

除了单个成员的偏移量要满足对齐规则,结构体整体的总大小(也就是最后一个成员的结束位置对应的偏移量)还需要满足 “整体对齐规则”:总大小必须是结构体内部最大基础数据类型成员大小的整数倍。这个 Test 结构体中最大的基础成员是 8 字节的 double,此时所有成员存放完毕后,累计占用的偏移量到了 21 字节(0 到 20),但 21 并不是 8 的整数倍,因此需要在 ch 之后填充 7 个字节的内存空洞,让结构体的总偏移量(总大小)达到 24 字节,24 是 8 的整数倍,完成整体对齐。

需要注意的是,偏移量的计算只和成员的数据类型、声明顺序以及编译器的对齐规则有关,和成员是否初始化(比如 sum=0dx=12.23)、是否通过构造函数初始化列表赋值都无关,这些初始化操作仅改变成员的初始值,不会影响偏移量的分配和内存空洞的产生;另外,成员函数(包括构造函数、普通成员函数)不占用结构体对象的内存,因此也不会参与偏移量的计算,偏移量仅针对非静态的成员变量。

每个特定平台上的编译器都有自己的默认“对齐系数”。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。

通过下列函数我们会发现p并不算指针,而是数据成员相对于对象的偏移量。这是因为普通指针(如 int*)存储的是变量在内存中的绝对地址。但类的成员属于模板,只有当类被实例化为对象后,成员才有具体的内存地址。

因此,int T::* 存储的是该成员在 T 对象中,相对于对象起始地址的字节偏移量,无论 T 实例化多少个对象,成员的偏移量都是固定的。

1
2
3
4
5
6
int main() {
int T::* p = nullptr; // 定义一个指向T类成员的指针
p = &T::sum; // 将指针指向T类的成员sum
p = &T::count; // 将指针p指向T类的成员count
return 0;
}

如果在VS中打开监视器,会发现p先被初始化为0xffffffff,紧接着由于被sum赋值为0x00000000,表示 sum 是 T 对象中第一个数据成员,偏移量为 0 字节。被count赋值为0x00000010,表示 count 相对于 T 对象起始地址的偏移量是 16 字节。

image-20260309192556669

因此想要直接通过 p 打印它指向的成员数据是不可能的,因为 sum count 是类的非静态数据成员,必须先实例化出类对象,才能访问到具体的成员数据。

1
2
3
4
5
6
7
8
int main() {
int T::* p = nullptr;
p = &T::sum;
p = &T::count;
T t(10); // 创建一个T类对象t,并初始化sum为10
cout << "t.*p: " << t.*p << endl; // 通过对象t和指针p访问成员count的值
return 0;
}

C++ 明确禁止重载的运算符有:.(成员访问符)、.*(指向成员的指针访问符)、::(作用域解析符)、sizeof(大小计算运算符)、typeid(类型信息运算符)、?:(三目条件运算符),此外#(预处理宏符)、##(宏连接符)因属于预处理阶段也无法重载,且->*虽可重载但.*不行。

对于函数指针,普通的函数指针将无法指向类成员函数,必须用类名修饰。

1
2
3
4
5
void func(int x) { cout << "func(int x): " << x << endl; }
int main() {
void (*func_ptr)(int) = func; // 函数指针
void (T:: * mem_func_ptr)() = &T::mem_func; // 类成员函数指针
}

倘若要进行调用,还需要用对象调用:(t.*mem_func_ptr)(20);

std::function

std::function 是个万能函数包装器,除了成员指针,几乎啥都能装。你只要指定函数签名(返回值和参数类型),它就能帮你保存、传递、延迟执行各种可调用对象。对于上面的各种对象,均可使用它来统一调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
std::function<void(int)> func_wrapper = func; // 使用std::function包装普通函数
func_wrapper(10);
Foo foo; // 仿函数
std::function<void(int)> foo_wrapper = foo; // 使用std::function包装仿函数
foo_wrapper(20);
Bar bar; // 可被转换为函数指针的类对象
std::function<void(int)> bar_wrapper = bar; // 使用std::function包装可转换为函数指针的类对象
bar_wrapper(30);
Test test; // 类对象
std::function<void(int)> mem_func_wrapper = std::bind(&Test::mem_func, &test, std::placeholders::_1); // 使用std::bind绑定类成员函数
mem_func_wrapper(40);
return 0;
}
1
2
3
4
func(int x): 10
Foo::operator(int x): 20
Bar::func(int x): 30
Test::mem_func(int x): x=40 a_=0

std::function 最常用的场景其实是做回调。比起传统函数指针灵活太多了。比如下面这个例子,直接把函数当参数传进来,按条件触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
// std::function作为函数入参
void call_when_even(int x, const function<void(int)>& f){
if (!(x % 2 == 0)) f(x);
}
void output(int x){
cout << x << endl;
}
int main(){
for (int i = 0; i < 10; ++i)
call_when_even(i, output);
cout << endl;
return 0;
}

这样写,0到9的偶数都会被 output 打印出来。std::function 灵活归灵活,但要想把成员函数指针、成员变量指针也一块儿塞进去,还得靠 std::bind。

std::bind

std::bind 的作用就是把可调用对象和参数“捆”在一起,生成一个新的可调用对象。你可以全绑定,也可以只绑定一部分参数,剩下的用占位符(std::placeholders::_1std::placeholders::_2 等)留着,等调用时再传。下面是几个常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void call_when_even(int x, const function<void(int)>& f){
if (!(x & 1)) f(x); // 相当于 if (x % 2 == 0) f(x);
}
void output(int x){
cout << x << endl;
}
void output_add_2(int x){
cout << x + 2 << endl;
}
int main(){
// 绑定output函数,使用占位符接收参数
auto fr1 = bind(output, placeholders::_1);
for (int i = 0; i < 10; ++i)
call_when_even(i, fr1);
// 绑定output_add_2函数,使用占位符接收参数
auto fr2 = bind(output_add_2, placeholders::_1);
for (int i = 0; i < 10; ++i)
call_when_even(i, fr2);
return 0;
}

上面 fr1、fr2 分别绑定了 output 和 output_add_2,偶数时分别打印原数和加2的结果。占位符用得好,参数顺序、固定参数都能随心所欲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void output(int x, int y){
cout << x << " " << y << endl;
}
int main(){
// 绑定所有参数,直接调用
bind(output, 1, 2)(); // 输出:1 2
// 绑定第二个参数,第一个参数用占位符接收
bind(output, placeholders::_1, 2)(1); // 输出:1 2
// 绑定第一个参数,第二个参数用占位符接收
bind(output, 2, placeholders::_1)(1); // 输出:2 1
// 绑定第一个参数,第二个参数用占位符_2,调用时需传入两个参数
bind(output, 2, placeholders::_2)(1, 2); // 输出:2 2
// 交换参数顺序
bind(output, placeholders::_2, placeholders::_1)(1, 2); // 输出:2 1
return 0;
}

对于占位符的顺序,是可以跳号使用(比如只用 _1_3,不用 _2),这是语法允许的,只要调用时传入的参数数量≥最大的占位符数字(比如用 _3 就必须传至少 3 个参数)。

1
2
3
4
5
6
7
8
void output(int a,int b) {
cout << "a: " << a << ", b: " << b << endl;
}
int main() {
auto f = std::bind(output, std::placeholders::_3, std::placeholders::_1);
f(1, 2, 3); // 输出 a: 3, b: 1
return 0;
}

std::bind 和 std::function 配合,连成员函数、成员变量都能统一操作。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test{
public:
int value = 0;
void output(int x, int y)
{
cout << x << " " << y << endl;
}
};

int main(){
Test test;
// 绑定类成员函数,第一个参数为类实例,后续为函数参数占位符
function<void(int, int)> fr1 = bind(&Test::output, &test, placeholders::_1, placeholders::_2);
fr1(1, 2); // 输出:1 2
// 绑定类成员变量,第一个参数为类实例
function<int&()> fr2 = bind(&Test::value, &test);
fr2() = 123; // 修改类成员变量的值
cout << test.value << endl; // 输出:123
return 0;
}

std::bind的简化与组合

以前 STL 里有 bind1st/bind2nd,写起来很别扭。std::bind 统一了写法,还能组合逻辑。比如查找大于5且小于等于10的元素个数:

1
2
3
4
5
6
7
8
9
int main(){
vector<int> col1 = {3, 7, 10, 12, 8, 5, 9};
// 组合bind函数,查找大于5且小于等于10的元素个数
using placeholders::_1;
auto f = bind(logical_and<bool>(), bind(greater<int>(), _1, 5), bind(less_equal<int>(), _1, 10));
int count = count_if(col1.begin(), col1.end(), f);
cout << "大于5且小于等于10的元素个数:" << count << endl; // 输出:4
return 0;
}

std::function实现原理

说了这么多,std::function 背后到底怎么实现的?其实核心思路很简单:用一个抽象基类做接口,模板子类包裹各种可调用对象,多态消除类型差异。下面我写了个极简版的仿 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 抽象基类,定义统一的调用接口
struct callable_base{
virtual void operator()() = 0;
virtual ~callable_base() {}
};

// 模板子类,封装具体的可调用对象
template<typename F>
struct callable : callable_base{
F _functor;
// 完美转发构造函数,接收可调用对象
callable(F&& functor) : _functor(forward<F>(functor)) {}
// 实现纯虚函数,转发调用
virtual void operator()() override{
_functor();
}
};
// 仿写的function类
struct my_function{
unique_ptr<callable_base> _callable;
// 模板构造函数,接收任意可调用对象
template<typename F>
my_function(F&& f) : _callable(new callable<F>(forward<F>(f))) {}
// 统一的调用接口
void operator()(){
(*_callable)();
}
};
// 测试用仿函数
struct obj{
void operator()(){
cout << "functor" << endl;
}
};

// 测试用普通函数
void func(){
cout << "function pointer" << endl;
}
int main(){
// 包装lambda表达式
my_function test1([]{ cout << "lambda" << endl; });
test1();
// 包装仿函数
obj obj1;
my_function test2(obj1);
test2();
// 包装普通函数
my_function test3(func);
test3();
return 0;
}

my_function 用 unique_ptr 持有 callable_base 指针,模板构造函数啥都能接,operator() 直接转发到底层对象。真正的 std::function 当然复杂得多,要支持可变参数、完美转发、内存优化啥的,但大体思路就是这样。

std::function与bind

总之,std::function 和 std::bind 让 C++ 写回调、保存各种函数对象都变得顺手多了。普通函数、仿函数、成员函数、成员变量,统统能统一处理,代码也更清爽。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 包装普通函数
int add(int a, int b) { return a + b; }
// 包装模板函数
template<class T>
const T add_template(const T& a, const T& b) { return a + b; }
// 包装仿函数
struct AddFunctor{
int operator()(int x, int y) const{
return x + y;
}
};
// 包装类静态成员函数
struct TestStatic{
public:
static const int AddStatic(const int x, const int y){
return x + y;
}
};
// 包装类成员函数
struct TestMember{
public:
const int AddMember(const int& x, const int& y){
return x + y;
}
};
int main(){
// 包装普通函数
function<int(int, int)> fr1 = add;
cout << fr1(12, 23) << endl; // 输出:35
// 包装模板函数
function<const int(const int&, const int&)> fr2 = add_template<int>;
cout << fr2(12, 23) << endl; // 输出:35
// 包装仿函数
function<int(int, int)> fr3 = AddFunctor();
cout << fr3(10, 20) << endl; // 输出:30
// 包装类静态成员函数
function<const int(const int&, const int&)> fr4 = TestStatic::AddStatic;
cout << fr4(10, 20) << endl; // 输出:30
// 包装类成员函数(结合bind)
TestMember test;
function<const int(const int&, const int&)> fr5 = bind(&TestMember::AddMember, &test, placeholders::_1, placeholders::_2);
cout << fr5(10, 20) << endl; // 输出:30
return 0;
}