C/C++|std::function 浅度解析

std::function 是 C++ 标准库中的一个通用多态函数包装器。它可以存储、复制和调用任意可调用目标(函数、lambda 表达式、绑定表达式或其他函数对象)。 std::function 占有固定尺寸的内存,这是因为它的实现方式决定了这一点。让我们深入探讨这一点。

std::function 的实现原理

std::function 通常使用类型擦除和小对象优化(Small Object Optimization, SOO)来实现这一点。

  1. 类型擦除:
  • std::function 使用类型擦除来存储不同类型的可调用对象。这意味着它通过一个固定大小的存储空间和一个指向这些对象的虚表(vtable)来实现多态性。
  • 类型擦除允许 std::function 在运行时处理各种不同的类型,而不需要知道这些类型的具体细节。
  1. 小对象优化(SOO):
  • 对于小对象(通常是指尺寸较小且可以直接存储在 std::function 内部的对象),std::function 会直接在其内部存储这些对象。这避免了动态内存分配的开销。
  • 对于大对象(超出 std::function 内部存储容量的对象),std::function 会在堆上分配内存,并在内部存储一个指向这些对象的指针。

std::function 为什么占有固定尺寸的内存

由于 std::function 使用了类型擦除和小对象优化,其内部实现通常包含以下几个部分:

  • 一个指向实际存储对象的指针或存储小对象的内部缓冲区。
  • 一个指向虚表的指针,用于多态调用。
  • 一些额外的元数据,用于管理存储和调用。

这意味着,无论存储的对象是多大或多小,std::function 的实例总是占用固定大小的内存,以包含这些指针和元数据。

内存布局的示例

假设我们有以下 std::function 声明:

cpp 复制代码
std::function<void()> func;

其内部可能包含如下内容:

  1. 指向可调用对象的指针 或 内部缓冲区:

    • 如果对象足够小,可以直接存储在内部缓冲区中。
    • 如果对象较大,则存储一个指向该对象的指针。
  2. 虚表指针:

    • 虚表指针指向一组函数,这些函数用于操作实际存储的对象(调用、复制、销毁等)。
  3. 元数据:

    • 用于管理存储对象的信息,如对象大小、类型信息等。

无论我们存储的是一个普通函数指针,一个小型 lambda 表达式,还是一个大型函数对象,std::function 实例的大小都是固定的

示例代码

cpp 复制代码
#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;
}

结果如下:

cpp 复制代码
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 通过类型擦除和虚表机制提供了类似的功能,使得它可以在运行时处理不同类型的可调用对象。

cpp 复制代码
#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 会根据存储的不同可调用对象类型,调用相应的实现。这种行为是运行时决定的,因此属于运行时多态。

相关推荐
董董灿是个攻城狮8 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
RuoZoe13 小时前
重塑WPF辉煌?基于DirectX 12的现代.NET UI框架Jalium
c语言
blasit15 小时前
笔记:Qt C++建立子线程做一个socket TCP常连接通信
c++·qt·tcp/ip
AI软著研究员15 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish15 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱16 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者1 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮1 天前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者1 天前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考1 天前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习