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

相关推荐
Demon--hx1 小时前
jsoncpp 核心概念梳理:命名空间、Value 容器与格式化输出
c++
徐小夕9 小时前
花了3个月,我们做了一款 AI 版的“飞书文档”
前端·算法·github
Robert--cao9 小时前
移动底盘与机械臂联合规划:开源算法与工程实现
算法
小豆庄10 小时前
Mac 上给视频人脸打码:本地工具的依赖、追踪与推理管线
算法·视觉检测·视频·敏捷流程·纹理贴图
星夜离尘5610 小时前
MATLAB 工程实验:从相邻块均值手写重叠 Allan 偏差,数清有效项
算法·matlab·均值算法
水饺编程12 小时前
第2章:Linux 基础,切换到超级用户
linux·c语言·汇编
y1su12 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
residual_fan13 小时前
船用柴油机动态机动工况故障诊断:控制感知仿射网络(CAAN)的技术实现
人工智能·算法·数据挖掘·数据分析
syagain_zsx13 小时前
BRpc 二次封装:Channel 管理、服务管理与回调工厂
c++·学习·程序人生·rpc·谷歌
其实防守也摸鱼13 小时前
Cylab在线靶场——CTF中的通用技能
linux·数据库·学习·算法·靶场·ctf·学习网络安全