C++ STL笔记01-std::function容器
std::function用法
std::function是一种通用、多态的函数封装。它允许对可调用目标实体进行存储、复制、和调用操作,而这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。
利用std::function
我们可以更容易地操作C++中的函数。比如说我们可以使用std::function
对象来包装函数(包括Labmda表达式、函数指针、结构体等),并实现函数调用:
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
#include <stdio.h>
#include <functional>
void func1(int x) {
printf("%d from function\n", x);
}
struct func_struct {
void operator() (int x) {
printf("%d from func_struct\n", x);
}
static void foo(int x) {
printf("%d from static member function\n", x);
}
};
int main()
{
// 函数
std::function<void(int)> func;
func = func1;
func(1);
// 函数指针
void (*func_ptr)(int);
func_ptr = func1;
func = func_ptr;
func(2);
// lambda表达式
auto func2 = [](int x) {printf("%d from lambda expression\n", x);};
func = func2;
func(3);
// 结构体(函数对象)
func = func_struct();
func(4);
// 静态成员函数
func = func_struct::foo;
func(5);
return 0;
}
运行上面的代码可以得到如下结果:
1
2
3
4
5
1 from function
2 from function
3 from lambda expression
4 from func_struct
5 from static member function
Function实现
接下来我们从零开始实现一个类似于标准库中std::function
的函数容器Function
。首先是模板定义:
1
2
3
4
5
6
7
8
9
10
template <class FnSig>
struct Function {
// 只在使用了不符合 Ret(Args...) 模式的 FnSig 时出错
static_assert(!std::is_same_v<FnSig, FnSig>, "not a valid function signature");
};
template <class Ret, class ...Args>
struct Function<Ret(Args...)> {
...
}
这里Function
模板有两个定义:第一个的定义是一个通用定义;而第二个的定义则是通用定义的模板特化,用来接收<Ret(Args...)>
形式的模板参数。这样做的意义在于只有当我们使用了<Ret(Args...)>
形式的模板参数才会生成具体的代码,否则会并在编译期报错。
需要额外说明的是模板参数<Ret(Args...)>
的意义:
-
Ret
表示函数的返回类型 -
Args...
表示一系列函数参数类型,其中省略号...
说明可以接受零个或多个参数并且构成一个参数包
因此<Ret(Args...)>
实际上是一个用于表示函数的返回类型和参数类型的函数签名。
下面看一下Function<Ret(Args...)>
的具体实现:
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
template <class Ret, class ...Args>
struct Function<Ret(Args...)> {
private:
struct FuncBase {
...
};
template <class F>
struct FuncImpl : FuncBase {
...
};
std::shared_ptr<FuncBase> m_base;
public:
Function() = default; // m_base 初始化为 nullptr
// 此处 enable_if_t 的作用:阻止 Function 从不可调用的对象中初始化
template <class F, class = std::enable_if_t<std::is_invocable_r_v<Ret, F &, Args...>>>
Function(F f) // 没有 explicit,允许 lambda 表达式隐式转换成 Function
: m_base(std::make_shared<FuncImpl<F>>(std::move(f)))
{}
Ret operator()(Args ...args) const {
if (!m_base) [[unlikely]]
throw std::runtime_error("function not initialized");
// 完美转发所有参数,这样即使 Args 中具有引用,也能不产生额外的拷贝开销
return m_base->call(std::forward<Args>(args)...);
}
};
首先,Function
中包含一个指针m_base
用来指向具体的函数对象实例。m_base
的类型是FuncBase
,它是一个函数基类我们先略过它的具体定义。
1
2
3
4
5
6
struct Function<Ret(Args...)> {
private:
...
std::shared_ptr<FuncBase> m_base;
...
}
构造函数Function(F f)
接收一个函数对象f
作为参数并使用m_base
来存储这个对象,此时会使用FuncImpl
来对F
类型进行封装。这里FuncImpl
是函数基类FuncBase
的实现,我们同样先略过它。需要注意的是在构造函数中还使用了std::enable_if
以及std::is_invocable_r_v
来判断F
类型是否可以调用并且满足函数签名<Ret(Args...)>
,只有满足条件的类型才能进行初始化。
1
2
3
4
5
6
7
8
public:
...
// 此处 enable_if_t 的作用:阻止 Function 从不可调用的对象中初始化
template <class F, class = std::enable_if_t<std::is_invocable_r_v<Ret, F &, Args...>>>
Function(F f) // 没有 explicit,允许 lambda 表达式隐式转换成 Function
: m_base(std::make_shared<FuncImpl<F>>(std::move(f)))
{}
...
除此之外Function
还重载了函数调用运算符,当m_base
非空时通过m_base
来执行实际的函数调用。
1
2
3
4
5
6
7
8
9
public:
...
Ret operator()(Args ...args) const {
if (!m_base) [[unlikely]]
throw std::runtime_error("function not initialized");
// 完美转发所有参数,这样即使 Args 中具有引用,也能不产生额外的拷贝开销
return m_base->call(std::forward<Args>(args)...);
}
...
最后我们来看一下FuncBase
和FuncImpl
的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct FuncBase {
virtual Ret call(Args ...args) = 0; // 类型擦除后的统一接口
virtual ~FuncBase() = default; // 应对F可能有非平凡析构的情况
};
template <class F>
struct FuncImpl : FuncBase { // FuncImpl 会被实例化多次,每个不同的仿函数类都产生一次实例化
F m_f;
FuncImpl(F f) : m_f(std::move(f)) {}
virtual Ret call(Args ...args) override {
// 完美转发所有参数给构造时保存的仿函数对象
return m_f(std::forward<Args>(args)...);
// 更规范的写法其实是:
// return std::invoke(m_f, std::forward<Args>(args)...);
// 但为了照顾初学者依然采用朴素的调用方法
}
};
FuncBase
是一个虚拟基类,它提供一个虚函数call
作为统一的接口;而FuncImpl
则是FuncBase
的具体实现,当我们调用Function
时,实际上是在调用FuncImpl
实现的call
函数。
总结一下,整个Function
的实现可以参考如下:
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
54
55
56
#pragma once
#include <iostream>
#include <utility>
#include <stdexcept>
#include <memory>
#include <type_traits>
#include <functional>
template <class FnSig>
struct Function {
// 只在使用了不符合 Ret(Args...) 模式的 FnSig 时出错
static_assert(!std::is_same_v<FnSig, FnSig>, "not a valid function signature");
};
template <class Ret, class ...Args>
struct Function<Ret(Args...)> {
private:
struct FuncBase {
virtual Ret call(Args ...args) = 0; // 类型擦除后的统一接口
virtual ~FuncBase() = default; // 应对F可能有非平凡析构的情况
};
template <class F>
struct FuncImpl : FuncBase { // FuncImpl 会被实例化多次,每个不同的仿函数类都产生一次实例化
F m_f;
FuncImpl(F f) : m_f(std::move(f)) {}
virtual Ret call(Args ...args) override {
// 完美转发所有参数给构造时保存的仿函数对象
return m_f(std::forward<Args>(args)...);
// 更规范的写法其实是:
// return std::invoke(m_f, std::forward<Args>(args)...);
// 但为了照顾初学者依然采用朴素的调用方法
}
};
std::shared_ptr<FuncBase> m_base; // 使用智能指针管理仿函数对象,用shared而不是unique是为了让Function支持拷贝
public:
Function() = default; // m_base 初始化为 nullptr
// 此处 enable_if_t 的作用:阻止 Function 从不可调用的对象中初始化
template <class F, class = std::enable_if_t<std::is_invocable_r_v<Ret, F &, Args...>>>
Function(F f) // 没有 explicit,允许 lambda 表达式隐式转换成 Function
: m_base(std::make_shared<FuncImpl<F>>(std::move(f)))
{}
Ret operator()(Args ...args) const {
if (!m_base) [[unlikely]]
throw std::runtime_error("function not initialized");
// 完美转发所有参数,这样即使 Args 中具有引用,也能不产生额外的拷贝开销
return m_base->call(std::forward<Args>(args)...);
}
};
Function
在进行实例化时,具有相同签名<Ret(Args...)>
的只会实例化出一份代码。因此只要可调用对象具有相同的签名,我们就可以使用同一个Function
类来进行处理;而对于具有相同签名但不同类型的函数对象,编译器会实例化出相应的FuncImpl
类而无需重新实例化整个Function
类。这种机制称为类型擦除,它允许在不同函数对象签名下实现通用性,同时保持类型安全和高效性。