std::function 原理简述

std::function 介绍

C++ 中有很多表示可调用代码段的方式,如函数指针、可调用类以及 lambda 表达式等,为了可以统一传递这些对象,我们可以通过函数模板的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void print(int i) {
std::cout << i << std::endl;
}

template<typename Func>
void doWithFunc(int n, Func f) {
for (int i=0; i<n; ++i) {
f(i);
}
}

int main() {
doWithFunc(10, print);
doWithFunc(10, [](int i) {
std::cout << i << std::endl;
});
doWithFunc(10, &print);
}

这样要求将函数实现成模板函数,比较麻烦。一个更好的替代方式是使用 C++ 标准库中的 std::function 对可调用对象进行包装,这样能够容易的做到统一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void print(int i) {
std::cout << i << std::endl;
}

void doWithFunc(int n, std::function<void(int)> f) {
for (int i=0; i<n; ++i) {
f(i);
}
}

int main() {
doWithFunc(10, print);
doWithFunc(10, [](int i) {
std::cout << i << std::endl;
});
doWithFunc(10, &print);
}

std::function 对象也是一个可调用对象,支持以下操作:

  1. 可调用,也就是重载了()运算符。
  2. 支持拷贝、移动以及赋值操作,这会将其中保存的可调用对象进行相应操作。
  3. 可以通过其它函数进行初始化或赋值,只要相应的函数签名能够匹配。
  4. 有一个合法的null状态,也就是没有函数和该对象绑定。

std::function 是 C++ 11标准中引入的,统一了可调用对象,使得回调函数实现等变得十分便利。具体用法十分简单,这里主要针对其实现原理进行介绍。

std::function 实现原理

FunctionPtr 设计

这里实现一个基本的 FunctionPtr 类,使用方法和 std::function 类一样,但是是一个简易的实现版本,目的在于将基本的原理描述清楚。

我们知道 FunctionPtr 是可调用对象的一个包装,换句话说,对于函数需要保存其函数指针,对于可调用类需要保存其一个实例,对于不同的对象需要保存不同的实例,这些实例都需要支持operator()接口,因此我们可以通过继承和多态来进行设计。FunctionPtr类持有一个FunctorBridge类的指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**  FunctionPtr  **/
template <typename Signature>
class FunctionPtr;

template <typename R, typename... Args>
class FunctionPtr<R(Args...)> {
private:
// bridge 指向具体的实现类,用于对不同的调用对象进行保存
FunctorBridge<R, Args...>* bridge;

public:
FunctionPtr()
: bridge(nullptr) {}
// TODO
FunctionPtr(const FunctionPtr& other);

FunctionPtr(FunctionPtr& other)
: FunctionPtr(static_cast<const FunctionPtr&>(other)) {}

FunctionPtr(FunctionPtr&& other)
: bridge(other.bridge) {
other.bridge = nullptr;
}
// TODO
template <typename F>
FunctionPtr(F&& f);

FunctionPtr& operator=(const FunctionPtr& other) {
FunctionPtr tmp(other);
swap(*this, tmp);
return *this;
}

FunctionPtr& operator=(FunctionPtr&& other) {
delete bridge;
bridge = other.bridge;
other.bridge = nullptr;
return *this;
}

template <typename F>
FunctionPtr& operator=(F&& f) {
FunctionPtr tmp(std::forward<F>(f));
swap(*this, tmp);
return *this;
}

~FunctionPtr() { delete bridge; }

friend void swap(FunctionPtr& fp1, FunctionPtr& fp2) { std::swap(fp1.bridge, fp2.bridge); }
// TODO
R operator()(Args... args) const;
};

这里将一些基本的框架进行了编写,包括拷贝、赋值以及移动相关的操作。这里留了 3 个最重要的函数没有实现,分别是:

  1. 拷贝构造函数FunctionPtr(const FunctionPtr& other);的具体实现,这里涉及到 bridge 指向对象的拷贝,因此我们后面介绍。
  2. 通过可调用对象进行初始化的构造函数的实现,这也是最重要的一个函数:
1
2
template <typename F>
FunctionPtr(F&& f);
  1. R operator()(Args... args) const;的实现,这也涉及到具体 FunctorBridge 类的原理。

FunctionBridge 继承结构

FunctionBridge是为了实现多态的基类,规定了一些接口:

1
2
3
4
5
6
7
template <typename T, typename... Args>
class FunctorBridge {
public:
virtual ~FunctorBridge() = default;
virtual FunctorBridge* clone() const = 0;
virtual T invoke(Args... args) const = 0;
};

其中 clone() 能够创建一个自身的拷贝,用于 FunctionPtr 拷贝构造函数实现深拷贝。invoke通过模板参数进行具体的可调用对象的调用。

具体的实现子类被设计成一个模板类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename Functor, typename R, typename... Args>
class SpecificFunctorBirdge : public FunctorBridge<R, Args...> {
private:
Functor functor;

public:
template <typename FunctorFwd>
SpecificFunctorBirdge(FunctorFwd&& functor)
: functor(std::forward<FunctorFwd>(functor)) {}

virtual SpecificFunctorBirdge* clone() const override {
return new SpecificFunctorBirdge(functor);
}

virtual R invoke(Args... args) const override {
return functor(std::forward<Args>(args)...);
}
};

这个模板类除了可调用对象返回类型R和参数Args...两个模板参数外,还增加了一个额外的模板参数Functor,表示对于不同可调用对象实际保存的类型。例如,对于函数对象,这里 Functor 就会是相应的函数指针,对于可调用类,Functor 就会是相应的类名,对于 lambda 表达时,Functor 就是相应匿名类的类型。

这个具体类很简单,保存了一个 Functor 类型的实例,构造函数通过完美转发调用Functor类相应的构造函数进行构造。clone()也就是创建了一个含有 functor 实例拷贝的对象,调用操作也就是简单的将参数进行完美转发调用。

最后再来补齐 FunctionPtr 类中 3 个遗留的最重要的成员函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 拷贝函数具体实现就是实现 bridge 的深拷贝,也就是将具体保存的可调用对象实例进行拷贝
template <typename R, typename... Args>
FunctionPtr<R(Args...)>::FunctionPtr(const FunctionPtr& other)
: bridge(nullptr) {
if (other.bridge) {
bridge = other.bridge->clone();
}
}
// 简单的转发调用
template <typename R, typename... Args>
R FunctionPtr<R(Args...)>::operator()(Args... args) const {
return bridge->invoke(std::forward<Args>(args)...);
}

// 通过用户传入的可调用对象进行 FunctionPtr 对象的构造,首先通过 `std::decay_t` 获得可调用
// 对象的退化,如函数会退化为相应函数指针,并移除调 cv 限定,让其适合存储(存储在 SpecificFunctorBirdge 类中)
template <typename R, typename... Args>
template <typename F>
FunctionPtr<R(Args...)>::FunctionPtr(F&& f)
: bridge(nullptr) {
using Functor = std::decay_t<F>;
bridge = new SpecificFunctorBirdge<Functor, R, Args...>(std::forward<F>(f));
}

编写简单的测试代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void print(int i) {
std::cout << i << std::endl;
}

void doWithFunc(int n, FunctionPtr<void(int)> f) {
for (int i=0; i<n; ++i) {
f(i);
}
}

int main() {
doWithFunc(10, print);
doWithFunc(10, [](int i) {
std::cout << i << std::endl;
});
doWithFunc(10, &print);
}

结果和标准库 std::function 一样,说明我们设计的 FunctionPtr 类具备相应的功能。

结论

这里实现的 FunctionPtr 类只是具备了 std::function 最核心的功能,也说明清楚了对于 C++ 中不同的可调用对象是如何进行保存的,也就是保存其相应的退化后的类型的实例。需要注意的是关于判断两个 FunctionPtr 类型保存的可调用对象是否一样还没有进行实现(也就是实现operator==operator!=),在《C++ Templates》中有具体的实现。