C/C++|std::function 浅度解析
std::function 是 C++ 标准库中的一个通用多态函数包装器。它可以存储、复制和调用任意可调用目标(函数、lambda 表达式、绑定表达式或其他函数对象)。std::function 占有固定尺寸的内存,这是因为它的实现方式决定了这一点。让我们深入探讨这一点。
std::function 是 C++ 标准库中的一个通用多态函数包装器。它可以存储、复制和调用任意可调用目标(函数、lambda 表达式、绑定表达式或其他函数对象)。 std::function 占有固定尺寸的内存,这是因为它的实现方式决定了这一点。让我们深入探讨这一点。
std::function 的实现原理
std::function 通常使用类型擦除和小对象优化(Small Object Optimization, SOO)来实现这一点。
- 类型擦除:
- std::function 使用类型擦除来存储不同类型的可调用对象。这意味着它通过一个固定大小的存储空间和一个指向这些对象的虚表(vtable)来实现多态性。
- 类型擦除允许 std::function 在运行时处理各种不同的类型,而不需要知道这些类型的具体细节。
- 小对象优化(SOO):
- 对于小对象(通常是指尺寸较小且可以直接存储在 std::function 内部的对象),std::function 会直接在其内部存储这些对象。这避免了动态内存分配的开销。
- 对于大对象(超出 std::function 内部存储容量的对象),std::function 会在堆上分配内存,并在内部存储一个指向这些对象的指针。
std::function 为什么占有固定尺寸的内存
由于 std::function 使用了类型擦除和小对象优化,其内部实现通常包含以下几个部分:
- 一个指向实际存储对象的指针或存储小对象的内部缓冲区。
- 一个指向虚表的指针,用于多态调用。
- 一些额外的元数据,用于管理存储和调用。
这意味着,无论存储的对象是多大或多小,std::function 的实例总是占用固定大小的内存,以包含这些指针和元数据。
内存布局的示例
假设我们有以下 std::function 声明:
std::function<void()> func;
其内部可能包含如下内容:
-
指向可调用对象的指针 或 内部缓冲区:
- 如果对象足够小,可以直接存储在内部缓冲区中。
- 如果对象较大,则存储一个指向该对象的指针。
-
虚表指针:
- 虚表指针指向一组函数,这些函数用于操作实际存储的对象(调用、复制、销毁等)。
-
元数据:
- 用于管理存储对象的信息,如对象大小、类型信息等。
无论我们存储的是一个普通函数指针,一个小型 lambda 表达式,还是一个大型函数对象,std::function 实例的大小都是固定的。
示例代码
#include <iostream>
#include <functional>
void exampleFunction() {
std::cout << "Hello from function!" << std::endl;
}
int main() {
// 存储普通函数指针
std::function<void()> func1 = exampleFunction;
// 存储lambda表达式
std::function<void()> func2 = []() {
std::cout << "Hello from lambda!" << std::endl;
};
// 存储大对象
struct LargeFunctor {
void operator()() const {
std::cout << "Hello from large functor!" << std::endl;
}
int data[100];
};
std::function<void()> func3 = LargeFunctor();
// 调用
func1();
func2();
func3();
std::cout << "Size of func1: " << sizeof(func1) << std::endl;
std::cout << "Size of func2: " << sizeof(func2) << std::endl;
std::cout << "Size of func3: " << sizeof(func3) << std::endl;
return 0;
}
结果如下:
Hello from function!
Hello from lambda!
Hello from large functor!
Size of func1: 32
Size of func2: 32
Size of func3: 32
我们可以很明显得看出三个 std::function 对象大小都是一样的。
小总结:
- std::function 的大小是固定的,因为它使用类型擦除和小对象优化来处理不同类型的可调用对象。
- 这种固定大小的内存布局使得 std::function 可以有效地存储和管理多种不同类型的可调用对象,同时提供统一的接口来调用这些对象。
- 无论存储的对象是函数指针、小型 lambda 表达式,还是大型函数对象,std::function 的实例大小都是固定的。
std::function 实现运行时多态
运行时多态(dynamic polymorphism)指的是在程序运行时决定调用哪个具体的函数实现。通常通过虚函数和继承来实现。std::function 通过类型擦除和虚表机制提供了类似的功能,使得它可以在运行时处理不同类型的可调用对象。
#include <iostream>
#include <functional>
// 一个接受 std::function<void(int)> 类型参数的函数
void invokeWithFive(const std::function<void(int)>& func) {
func(5); // 调用传入的可调用对象,并传递参数5
}
int main() {
// 使用普通函数指针
void (*funcPtr)(int) = [](int x) { std::cout << "Function pointer: " << x << std::endl; };
invokeWithFive(funcPtr);
// 使用 lambda 表达式
auto lambda = [](int x) { std::cout << "Lambda: " << x << std::endl; };
invokeWithFive(lambda);
// 使用函数对象
struct Functor {
void operator()(int x) const {
std::cout << "Functor: " << x << std::endl;
}
};
Functor functor;
invokeWithFive(functor);
return 0;
}
在运行时,当我们调用 invokeWithFive(funcPtr)、invokeWithFive(lambda) 和 invokeWithFive(functor) 时,std::function 会根据存储的不同可调用对象类型,调用相应的实现。这种行为是运行时决定的,因此属于运行时多态。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)