• 搜索赖思理的个人网站

  • View in English (切换至英文)English
  • C++中的std::function到底是什么,为什么我们需要它?

    创建时间:2021年1月18日最近修改时间:2021年12月19日

    昨天,有人在#include<C++>的discord服务器上问了关于为社么我们需要std::function的问题。 下面是我对这个问题的回答。

    即使参数类型以及返回类型都完全相同,C++中的可以被当作函数一样被调用的对象也可以有不同的类型

    Lambda表达式可以被认为是定义有operator()的类的语法糖。比如说

    int x = 3;
    auto lambda = [x](int y) { return x + y; };

    大体上等同于

    struct __Lambda {
    int x;
    int operator()(int y) const {
    return x + y;
    }
    };
    int x = 3;
    auto lambda = __Lambda { .x = x };

    因此,每个lambda表达式都有一个独特的类型。例如,在下面的片段中,

    int x, z;
    auto lambda = [x](int y) { return x + y; };
    auto lambda2 = [x, z](int y) { return x + y + z; };

    尽管lambdalambda2都接收一个int并返回一个int,它们有着不一样的类型。

    C++还有普通的函数, 它们又和实现了operator()的类不同。

    std::function的需求

    被当作函数一样被调用的对象

    那么,我们如何存储一个接收一个int并返回一个int的一个可调用的对象,并且不考虑它的具体类型?

    我们需要std::function来完成这样的任务。比如说:

    struct S {
    std::function<int(int)> func;
    };

    以这种方式存储可调用程序的典型用例是一个task system。 你可能想在一个容器中存储回调,以便于以后执行:

    struct TaskQueue {
    std::queue<std::function<void()>> queue;
    std::mutex mutex;
    std::condition_variable ready;
    // 我省略了各个成员函数的实现
    ...
    };

    类型擦除(Type Erasure)

    为了使func同时接受lambdalambda2, std::function需要有可以接受任何符合要求的函数对象或普通函数的构造函数。 我们需要使用类型擦除来实现这种行为。

    在C++中实现类型擦除的方法有很多,不过这对于这篇文章来说有些超纲。 大体上的想法是,std::function需要储存一个函数指针以及另外一些用于存放lambda捕获的空间。 因为lambda表达式(或函数对象)可以有任意大小的捕获,这些额外的数据需要在堆上分配。 不过,所有主要的std::function实现都会进行小缓冲区优化(small buffer optimization),如果你的lambda小到可以装入预定义的容量。 在这种情况下,所有的数据都可以直接在std::function对象本身内部分配,而不需要进行额外的堆分配。