C++中的std::function到底是什么,为什么我们需要它?

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

昨天,有人在#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对象本身内部分配,而不需要进行额外的堆分配。