move_only_function是C++23引入的类型擦除容器,它与function最大的不同就是前者可以移动move,具有更好的效率。

源码深究(MSVC)

在STL里面,move_only_function有三层架构设计:

1
2
3
4
5
move_only_function (用户接口层)

_Move_only_function_call (调用语义层)

_Move_only_function_base (存储和管理层)

move_only_function用户层,这也是我们使用时候直接用到的

_Move_only_function_call中间层,这一层处理了noexceptconst还有&|&&修饰符,因此支持move_only_function<void()const noexcept>这样的写法

_Move_only_function_base底层,负责底层的内存管理,调用分发。

手搓虚函数表

move_only_function手搓了一个类似于虚函数表的东西,具体结构是:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <bool>
struct _Invoke_t {
using _Call = _Rx(__stdcall*)(const _Move_only_function_data&, _Types&&...);
};
template <>
struct _Invoke_t<true> {
using _Call = _Rx(__stdcall*)(const _Move_only_function_data&, _Types&&...) _NOEXCEPT_FNPTR;
};
struct _Impl_t {
_Invoke_t<_Noexcept>::_Call _Invoke;
void(__stdcall* _Move)(_Move_only_function_data&, _Move_only_function_data&) _NOEXCEPT_FNPTR;
void(__stdcall* _Destroy)(_Move_only_function_data&) _NOEXCEPT_FNPTR;
};

这3个成员都是函数指针,对于不同的函数大小设置不同的函数。

_Invoke_t<_Noexcept>根据是否为noexcept特化不同的函数版本。只看这个代码可能看不出为什么手搓一个虚函数表,我们用虚函数表实现一下:

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
// 传统虚函数方式(简化版)
template<class Rx, class... Types>
class move_only_function_traditional {
// 抽象基类
struct CallableBase {
virtual ~CallableBase() = default;
virtual Rx invoke(Types... args) = 0;
virtual void move_to(void* dest) = 0;
};

// 每个具体类型都需要一个派生类
template<class Callable>
struct CallableImpl : CallableBase {
Callable func;

CallableImpl(Callable f) : func(std::move(f)) {}

Rx invoke(Types... args) override {
return func(std::forward<Types>(args)...);
}

void move_to(void* dest) override {
new (dest) CallableImpl(std::move(func));
}
};

CallableBase* ptr; // 必须用指针,无法内联存储
};

用C++自身的虚函数实现是相当简明的,对于不同类型,实例化不同的CallableImpl,在抽象基类CallableBase调用的invokemove_toCallableImpl里面的,这里也是实现了类型擦除。不过,MSVC采用手搓虚函数表还是有其道理的:

  • 减少RTTI数据,如果使用virtual的类,编译器会为其生成RTTI数据,还有type_infodynamic_cast等,还有虚函数表。对于每一个lambda,编译器会解析为不同的类型;如果lambda的数量多起来,RTTI数据会非常庞大。
  • trivial类型优化,如果是trivial类型,手动虚函数表可以将函数设置为nullptr,无需再实例化一个CallableImpl
  • 为调用dll函数提供安全保障。
  • 实现内联存储,一般的类型擦除,都会采用void*在堆上开辟新空间,手搓虚函数表的方式可以实现内联存储,这也是接下来要讲解的部分。

小对象优化

小对象优化(SBO - Small Buffer Optimization)是move_only_function实现非常巧妙的一个部分。在MSVC中,用于存储函数的对象是:

1
2
3
4
5
union _Move_only_function_data {
void* _Pointers[_Small_object_num_ptrs];
const void* _Impl;
char _Data;
};

对于小的对象,move_only_function会将其直接存储到栈空间上(内联存储),如果是大对象,那么会使用new开辟堆空间存放。使用栈空间内联存储。

对于上面的结构,move_only_function存储方式是这样的(这里假设_Small_object_num_ptrs=4,指针大小为8字节):

1
2
3
4
5
6
7
// 这里假设alignas = 4
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | _Impl | 可用缓冲区(24 字节) |
// | 8 字节 | |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// _Buf_offset = 8
// _Buf_size = 32 - 8 = 24 字节

一开始看到可能会有点疑惑,明明_Impl_t有3个函数指针,大小应该是24字节,这个我们先留着,后面再解释。

判断是否可以内联存储的有3个条件:

1
2
3
4
5
template <class _Vt>
static constexpr bool _Large_function_engaged =
alignof(_Vt) > alignof(max_align_t)
|| sizeof(_Vt) > _Move_only_function_data::_Buf_size<_Vt>
|| !is_nothrow_move_constructible_v<_Vt>;

从上到下依次是:

  • 对齐要求
  • 大小要求
  • 移动异常安全性

内存对齐

这里先补充一下关于内存对齐的知识。内存对齐算是平时写C++感知不强,但是在底层开发与性能优化时是绝对绕不开的话题。

在我们的直觉里,内存就像一个连续的字节数组,CPU 可以随意从任意地址读取任意大小的数据。但实际上并非如此。

1
2
3
4
|-------------------------------|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|-------------------------------|
|0x0 |0x8

为了硬件层面的高效率,CPU 读取内存是 按块(Chunks) 读取的(比如每次读 4 字节、8 字节或 16 字节)。

1
2
3
4
|---------------|---------------|---------------|
| 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 |
|---------------|---------------|---------------|
|0x0 |0x4 |0x8
  • 对齐的内存: 如果一个 4 字节的 int 刚好放在地址为 4 的倍数(0, 4, 8, 12…)的地方,CPU 一次内存读取就能把它完整拿出来。
  • 不对齐的内存: 如果这个 int 存放在了地址为 1 的地方(跨越了块的边界),CPU 可能需要读取两次内存,然后通过内部的位移指令拼接出这个数据。这会导致严重的性能惩罚。在某些架构(如早期的 ARM)上,读取未对齐的内存甚至会直接触发硬件异常导致程序崩溃

因此,C++编译器会为每一种数据类型规定一个对齐要求,int是4字节对齐,double为8字节对齐。我们可以手动指明对齐大小:

1
2
3
4
struct alignas(16) Align16{
char b;
int x;
} //这个数据结构大小为16

回到move_only_function的实现上来,C++中的max_align_t通常对齐到16字节,如果类型对其要求更加严格,例如 AVX-512需要64字节对齐(这种属于 过度对齐类型 over-aligned type),那么就需要堆分配,栈上的缓冲区无法保证这种对齐。

在C++17之后,若类型没有实现专属的operator new(如果实现了,但是支持对齐感知),new返回的地址满足alignof(T)的对齐要求。

1
2
3
4
5
6
7
8
9
    void* _Ptr;
#ifdef __cpp_aligned_new //C++17标准
if constexpr (alignof(_Vt) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { //宏的值一般是16
_Ptr = ::operator new(sizeof(_Vt), align_val_t{alignof(_Vt)});
} else
#endif // defined(__cpp_aligned_new)
{
_Ptr = ::operator new(sizeof(_Vt));
}

大小要求

这个条件很好理解,如果栈空间塞不下去,那么就需要在堆中申请空间。

_Move_only_function_data联合体对象里,实现了一些模板变量,用于计算可用的空间

1
2
3
4
5
6
7
8
9
10
11
// _Buf_size 计算可用缓冲区大小
template <class _Fn>
static constexpr size_t _Buf_size = sizeof(_Pointers) - _Buf_offset<_Fn>;
// ↑ ↑
// 总大小 减去前面的偏移

// _Buf_offset 计算存储位置的偏移
template <class _Fn>
static constexpr size_t _Buf_offset =
alignof(_Fn) <= sizeof(_Impl) ? sizeof(_Impl) // 小对齐:紧跟 _Impl
: alignof(_Fn); // 大对齐:需要填充

这里有两种内存对齐情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 假设 _Small_object_num_ptrs = 4,每个指针 8 字节,总共 32 字节

// 情况 A:小对齐的类型(alignof = 4)
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | _Impl | 可用缓冲区(24 字节) |
// | 8 字节 | |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// _Buf_offset = 8
// _Buf_size = 32 - 8 = 24 字节

// 情况 B:大对齐的类型(alignof = 16)
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | _Impl | padding| 可用缓冲区(16 字节) |
// | 8 字节 | 8 字节 | |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// _Buf_offset = 16(需要对齐到 16)
// _Buf_size = 32 - 16 = 16 字节

更大的对齐类型就放在堆里面了

移动异常安全

C++标准要求move_only_function必须是移动无异常。如果对象的移动函数并非noexcept,那么就只能采用堆构造。堆构造的移动并不会调用对象的移动构造函数,栈构造的对象需要移动则必须使用对象的移动构造函数,如果并不是noexcept,则与标准要求的相互违背。

构造时的路径选择

构造从这个函数开始,这个函数在move_only_function构造函数中被调用。这里根据是否适合在栈中构造做了路径选择。

1
2
3
4
5
6
7
8
9
10
11
template <class _Vt, class _VtInvQuals, class... _CTypes>
void _Construct_with_fn(_CTypes&&... _Args) {
_Data._Impl = _Create_impl_ptr<_Vt, _VtInvQuals>();//这里_Data就是_Move_only_function_data
if constexpr (_Large_function_engaged<_Vt>) {
//堆中构造
_Data._Set_large_fn_ptr(_STD _Function_new_large<_Vt>(_STD forward<_CTypes>(_Args)...));
} else {
//栈中构造
::new (_Data._Buf_ptr<_Vt>()) _Vt(_STD forward<_CTypes>(_Args)...);
}
}

接下来看_Create_impl_ptr,这个函数创建了一个静态常量,用来存放虚表,生命周期和程序一样长。这样对于相同类型的函数对象,虚表都是同一份,这个和C++多态实现虚表是同样的操作。因此,上面_Impl_t有24字节,可是Data里面只留了8字节的原因就是Data里面存储的是_Impl_t全局静态对象的指针,避免了每一个实例对象都一份,节约了内存空间。

1
2
3
4
5
template <class _Vt, class _VtInvQuals>
_NODISCARD static const _Impl_t* _Create_impl_ptr() noexcept {
static constexpr _Impl_t _Impl = _Create_impl<_Vt, _VtInvQuals>();
return &_Impl;
}

里面的_Create_impl是构造虚表的函数

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
template <class _Vt, class _VtInvQuals>
_NODISCARD static constexpr _Impl_t _Create_impl() noexcept {
_Impl_t _Impl{};

// === 第一层分支:大对象 vs 小对象 ===
if constexpr (_Large_function_engaged<_Vt>) {
// 大对象路径
_Impl._Invoke = _Function_inv_large<_Vt, _VtInvQuals, _Rx, _Noexcept, _Types...>;
_Impl._Move = nullptr; // 大对象移动很简单,直接复制指针

// === 第二层分支:析构优化 ===
if constexpr (is_trivially_destructible_v<_Vt>) {
// Trivial 析构:只需要释放内存
// 同时,如果是过度对齐类型,则采用特化的delete(C++17引入)
if constexpr (alignof(_Vt) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
_Impl._Destroy = _Function_deallocate_large_default_aligned;
} else {
_Impl._Destroy = _Function_deallocate_large_overaligned<alignof(_Vt)>;
}
} else {
// 非 Trivial 析构:需要调用析构函数
_Impl._Destroy = _Function_destroy_large<_Vt>;
}
} else {
// 小对象路径
_Impl._Invoke = _Function_inv_small<_Vt, _VtInvQuals, _Rx, _Noexcept, _Types...>;

// === 第三层分支:移动优化 ===
if constexpr (is_trivially_copyable_v<_Vt> && is_trivially_destructible_v<_Vt>) {
// Trivially copyable:可以用 memcpy
if constexpr ((_Function_small_copy_size<_Vt>) > _Minimum_function_size) {
_Impl._Move = _Function_move_memcpy<_Function_small_copy_size<_Vt>>;
} else {
_Impl._Move = nullptr; // 太小了,直接复制指针更快
}
} else {
// 需要调用移动构造函数
_Impl._Move = _Function_move_small<_Vt>;
}

// === 第四层分支:析构优化 ===
if constexpr (is_trivially_destructible_v<_Vt>) {
_Impl._Destroy = nullptr; // 无需析构
} else {
_Impl._Destroy = _Function_destroy_small<_Vt>;
}
}

return _Impl;
}

对于_Move_Destory指针,非空则调用;如果为空,_Move直接复制指针,_Destory则不做任何处理。

_Destory调用:

1
2
3
4
5
6
static void _Checked_destroy(_Move_only_function_data& _Data) noexcept {
const auto _Impl = _Get_impl(_Data);
if (_Impl->_Destroy) {
_Impl->_Destroy(_Data);
}
}

_Move调用:

1
2
3
4
5
6
7
8
9
static void _Checked_move
(_Move_only_function_data& _Data, _Move_only_function_data& _Src) noexcept {
const auto _Impl = _Get_impl(_Src);
if (_Impl->_Move) {
_Impl->_Move(_Data, _Src);
} else {
_Function_move_large(_Data, _Src);
}
}

手搓一个

由于写线程池需要,因此手搓一个move only function。代码其实很多都是参考MSVC STL里面的,包括设计也是。

设计

写一个高(低)性能库,首先是要设计好。这个工程命令为mfunction。设计图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mfunction<Sign...>
|
|
call layer
- operator()
- 限定符的适配
|
base layer
- 构造虚表
- 构造SBO与堆
- 移动与交换
|
data pack
- 包含虚表和SBO缓存区

对象存储函数会分为两种情况,一种是堆中,另一种是在栈中。同时,如果初始化没有给任何函数,那么就是空状态。

  • Empty : 空状态 vtab = nullptr
  • Stack : 栈中 vtab存在,storage 内部保存了可调用对象
  • Heap: 堆中 vtab存在,storage 内部保存了可调用对象地址

Base层状态机

考虑一种状态机,在base层

construct函数,构造虚表和缓存对象

  • 成功,vtab和stroage都有效
  • 失败,保持empty

destory销毁自身

  • 如果虚表存在,那么调用销毁函数,并且重置虚表
  • 虚表不存在,空对象,不处理

move_from:只是将other移动到自身,但是不考虑自身是否有对象。被转移的对象就是空对象

  • 如果自身为空,原地构建。如果other为空,清空自己
  • 如果非空,那么就是UB

这里我们需要处理好 base::move_from和虚表的_Move,虚表_Move处理的是缓存对象的移动。

assignmove_from升级版本

  • 如果自身为空,走move_from
  • 非空,先销毁自身,再走move_from

各种模板参数名称:

  • _Fx 函数类型
  • _Sign... 类型包,用于最顶层,再使用caller进行萃取
  • _Rx 返回值
  • _Ax...参数类型包
  • _Dx 函数类型的退化std::decay_t<_Fx>
  • _OrDx 加上限定符的函数类型,需要通过caller层的_Origial<_Dx>获取

斩杀线

到底是在栈中内联存储还是走堆分配的路线,通过一个模板变量来判断。栈中内联存储有以下要求:

  • 对齐,超过max_align_t不能内联存储
  • 对象大小,超过设定值不能内联存储

其实也可以再加一条,保证移动构造函数noexcept,这个不强求,添加了需要修改虚表的_Movenoexcept属性。

缓存对象

针对小对象的优化,避免堆分配消耗过多时间。缓存大小可以设置,默认就是4*8 = 32 Bytes,

1
2
3
4
union alignas(std::max_align_t) _SBO_Storage{
void* ptr[4];
char data;
}storage;

设计联合体加个char主要是为了方便取地址。

处于empty状态时,里面的值是不确定的。

处于stack时,ptr数组里面存储的就是可调用对象,通过&data获取到函数地址。

处于heap时,ptr数组的第一个元素存储的是可调用对象地址,通过ptr[0]获取。

_SBO_Storage只处理最底层的数据表示:

  • set_heap_ptr 设置存储的地址
  • heap_ptr获取存储的地址
  • stack_ptr获取存储的地址

上述的3个函数都是函数模板,需要传入具体的函数类型_Dx。栈中内联需要外部自行构建。

模拟虚表

参考了STL的设计,我们写成_Invoke,_Move,_Destory三种。根据不同的_Dx(std::decay_t<_Fx>),我们生成不同的虚表,生成虚表需要最原始的_Rx(_Ax...)模板。

1
2
3
4
5
6
template<class _Rx,class... _Ax>
struct {
_Rx(*_Invoke)(SBO&,_Ax&&...);//这里noexcept需要从call layer中萃取
void(*_Move)(SBO&,SBO&); //dest,src 这里不一定noexcept,
void(*_Destory)(SBO&)noexcept;
}

虚表处理的只是_SBO_Storage的调用、移动和析构操作。

虚表中需要处理的是如何原地构造,并且需要注意noexcept,还有over_align过度对齐类型。

_Invoke,需要提供_Dx,_Rx,_Ax,是否Noexcept,还有_OrDx,要实现限定符保留,必须使用_OrDx对函数对象强制类型转换static_cast<_OrDx>(*ptr)

_Move,移动srcdest,会调用src的析构函数,默认dest就是空对象。

_Destory,销毁传入的对象,先析构再delete

这里我们只关注_SBO_Storage的操作,不越界操作虚表。同时,要说明的是,只是调用_Move函数实现移动是不完整的操作,并不能实现3个状态之间的转移。

堆情况:

  • _Move函数直接复制指针即可,src参数不进行任何处理(也可以置空,但是没必要)
  • _Destory销毁函数指针,会调用析构函数(可以对平凡类型跳过析构

栈情况:

  • _Move 需要调用可调用对象的移动构造函数,并且对src参数进行析构
  • _Destory 调用可调用对象的析构函数。

后续我们可以针对上面进行优化。

虚表在Base层进行构建,_create_vtable创建虚表,_get_vtable_ptr获取到虚表的指针。虚表是每个_OrDx一份,因此在_get_vtable_ptr静态成员函数定义一个静态变量,返回这个静态变量的地址。

限定符保留

在调用函数时,我们要保留各种限定符,包括&``noexcept``const,保留放在call layercaller定义了仿函数,函数就带上了上述的修饰符。这个会影响虚表_Invoke,我们需要在caller里面定义一个模板类型,传入的类型加上萃取的限定符(定义为_OrDx (Origial Decay_t Fx))。这个会在mfunction层的构造函数使用到。

保留各种限定符号的方法就是偏特化,重载上述限定符的各种组合。

源码实现

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#ifndef _FR_PACKAGEv3_HEADER
#define _FR_PACKAGEv3_HEADER
#include <exception>
#include <functional>

namespace packagev3 {
namespace detail {

constexpr size_t Storage_Size = 4;

//是否为过度对齐
template<class _Fx>
constexpr bool is_over_aligned = alignof(_Fx) > alignof(std::max_align_t);
//是否需要走堆分配路线
template<class _Fx>
constexpr bool require_on_heap = is_over_aligned<_Fx>
|| sizeof(_Fx) > sizeof(void*) * Storage_Size;

//TODO add noexcept
template<class _Rx, class... _Ax>
struct _MFPack {
union alignas(std::max_align_t) _sbo_storage {
void* ptr[Storage_Size];
char data;

template<class _Dx>
void set_heap(_Dx* const _func)noexcept {
ptr[0] = _func;
}

template<class _Dx>
_Dx* stack_ptr()const noexcept {
return reinterpret_cast<_Dx*>(&(const_cast<_sbo_storage*>(this)->data));
}

template<class _Dx>
_Dx* heap_ptr()const noexcept {
return static_cast<_Dx*>(ptr[0]);
}

}storage;

struct _virtual_table {
_Rx(*_Invoke)(const _sbo_storage&, _Ax&&...);
void(*_Move)(_sbo_storage&, _sbo_storage&);
void(*_Destory)(_sbo_storage&) noexcept; //析构是noexcept
}*vtab = nullptr;
/*
* 判空时,不能只通过storage里面的内容进行判断,里面的内容是不确定的
* 需要结合vtab的属性进行判断。如果vtab为空,则storage内容无意义
*/
};

template<class _Fx, class _Dx>
void* _Alloca_Heap(_Fx&& _Fn) {
struct _Guard {
void* ptr = nullptr;

~_Guard() {
if (ptr) {
if constexpr (is_over_aligned<_Dx>) {
::operator delete(ptr, std::align_val_t{ alignof(_Dx) });
} else {
::operator delete(ptr);
}
}
}
};

_Guard guard;
void* palloc;
if constexpr (is_over_aligned<_Dx>) {
palloc = ::operator new(sizeof(_Dx), std::align_val_t{ alignof(_Dx) });
} else {
palloc = ::operator new(sizeof(_Dx));
}
guard.ptr = palloc;
//构造
::new(palloc) _Dx(std::forward<_Fx>(_Fn));
guard.ptr = nullptr;
return palloc;
}

template<class _Rx, class... _Ax>
_Rx _Invalid_Function(const typename _MFPack<_Rx, _Ax...>::_sbo_storage&,
_Ax&&...) {
//看情况再设置不同的异常效果
throw std::exception("Invaild function.");
}

template<class _Dx, class _OrDx, class _Rx, class... _Ax>
_Rx _Invoke_Stack(const typename _MFPack<_Rx, _Ax...>::_sbo_storage& _storage,
_Ax&&... _args) {
if constexpr (std::is_void_v<_Rx>) {
std::invoke(static_cast<_OrDx>(*_storage.stack_ptr<_Dx>()),
std::forward<_Ax>(_args)...);
} else {
return std::invoke(static_cast<_OrDx>(*_storage.stack_ptr<_Dx>()),
std::forward<_Ax>(_args)...);
}
}
template<class _Dx, class _OrDx, class _Rx, class... _Ax>
_Rx _Invoke_Heap(const typename _MFPack<_Rx, _Ax...>::_sbo_storage& _storage,
_Ax&&... _args) {
if constexpr (std::is_void_v<_Rx>) {
std::invoke(static_cast<_OrDx>(*_storage.heap_ptr<_Dx>()),
std::forward<_Ax>(_args)...);
} else {
return std::invoke(static_cast<_OrDx>(*_storage.heap_ptr<_Dx>()),
std::forward<_Ax>(_args)...);
}
}
//void(*_Move)(_sbo_storage&, _sbo_storage&);

template<class _Rx, class... _Ax>
void _Move_heap(typename _MFPack<_Rx, _Ax...>::_sbo_storage& _dest,
typename _MFPack<_Rx, _Ax...>::_sbo_storage& _src) {
//使用memcpy复制
memcpy(&_dest.data, &_src.data, sizeof(typename _MFPack<_Rx, _Ax...>::_sbo_storage));
}
//调用对象的移动构造函数,再析构src
template<class _Dx, class _Rx, class... _Ax>
void _Move_stack(typename _MFPack<_Rx, _Ax...>::_sbo_storage& _dest,
typename _MFPack<_Rx, _Ax...>::_sbo_storage& _src) {
auto src_ptr = _src.stack_ptr<_Dx>();
::new(_dest.stack_ptr<_Dx>()) _Dx(std::move(*src_ptr));
src_ptr->~_Dx();
}
template<class _Dx, class _Rx, class... _Ax>
void _Destory_heap(typename _MFPack<_Rx, _Ax...>::_sbo_storage& _dest) noexcept{
auto obj = _dest.heap_ptr<_Dx>();
obj->~_Dx();
if constexpr (is_over_aligned<_Dx>) {
::operator delete (obj, std::align_val_t{ alignof(_Dx) });
} else {
::operator delete (obj);
}
}

template<class _Dx, class _Rx, class... _Ax>
void _Destory_stack(typename _MFPack<_Rx, _Ax...>::_sbo_storage& _dest) noexcept{
auto obj = _dest.stack_ptr<_Dx>();
obj->~_Dx();
}

template<class _Rx,class... _Ax>
class _MFBase {
using _MyPack = _MFPack<_Rx, _Ax...>;
using _MyTable = typename _MyPack::_virtual_table;
using _MyStorage = typename _MyPack::_sbo_storage;

//构建虚表
template<class _Dx,class _OrDx>
static constexpr _MyTable _create_vtable() {
_MyTable table{ nullptr,nullptr,nullptr };

if constexpr (require_on_heap<_Dx>) {
table._Invoke = _Invoke_Heap<_Dx, _OrDx, _Rx, _Ax...>;
//堆移动,我们移动一个整个storage,memcpy
table._Move = _Move_heap<_Rx, _Ax...>;
//堆销毁,调用析构函数,并且delete
table._Destory = _Destory_heap<_Dx, _Rx, _Ax...>;
} else {
table._Invoke = _Invoke_Stack<_Dx, _OrDx, _Rx, _Ax...>;
table._Move = _Move_stack<_Dx, _Rx, _Ax...>;
table._Destory = _Destory_stack<_Dx, _Rx, _Ax...>;
}
return table;
}

template<class _Dx, class _OrDx>
static constexpr const _MyTable* _get_vtable_ptr() {
static constexpr _MyTable tab = _create_vtable<_Dx, _OrDx>();
return &tab;
}

public:
const _MyTable* vtable()const noexcept {
return pack.vtab;
}

_MyStorage& storage() noexcept {
return pack.storage;
}
//销毁自身数据,转换为 empty状态
void destory() noexcept{
if (!empty()) {
pack.vtab->_Destory(pack.storage);
pack.vtab = nullptr;
}
}
//将_other移动到自身,默认自身为空
void move_from(_MyPack&& _other) {
if (_other.vtab != nullptr) {
_other.vtab->_Move(pack.storage, _other.storage);
pack.vtab = _other.vtab;
_other.vtab = nullptr;//这里我们不可以使用destory 会导致双重释放
}
}
void assign(_MFBase&& _other) {
if (!empty()) {
this->destory();
}
move_from(std::move(_other.pack));
}
//isNoexcept
template<class _Fx,class _Dx, class _OrDx>
void construct(_Fx&& _Fn) {
//设置缓存
if constexpr (require_on_heap<_Dx>) {
//堆分配道路
pack.storage.set_heap(_Alloca_Heap<_Fx, _Dx>(std::forward<_Fx>(_Fn)));
} else {
//栈内联
std::construct_at(pack.storage.stack_ptr<_Dx>(), std::forward<_Fx>(_Fn));
}
//创建虚表
//这里去除了const修饰,但是保证vtab里面的成员不会被修改,只会有vtab = *这种操作。
pack.vtab = const_cast<_MyTable*>(_get_vtable_ptr<_Dx, _OrDx>());
}

bool empty() const noexcept{
return pack.vtab == nullptr;
}
//安全获得invoke
auto get_invoke()const -> _Rx(*)(const _MyStorage&, _Ax&&...) {
if (pack.vtab == nullptr) {
//返回一个抛出异常的函数
return &_Invalid_Function<_Rx, _Ax...>;
}
return pack.vtab->_Invoke;
}

_MyPack pack;
};

template<class... _Sign>
class _MFCall {
static_assert(sizeof...(_Sign) == 0&&false, "Wrong function type");
};

//萃取 const noexcept &
template<class _Rx,class..._Ax>
class _MFCall<_Rx(_Ax...)>:public _MFBase<_Rx,_Ax...> {
public:
template<class _Ty>
using _Origial = _Ty&;

template<class _Dx>
static constexpr bool _Invocable = std::is_invocable_r_v<_Rx, _Dx, _Ax...>;
//可调用对象
_Rx operator()(_Ax... args) {
return this->get_invoke()(this->storage(), std::forward<_Ax>(args)...);
}
};


template<class... _Sign>
class mfunction :private _MFCall<_Sign...> {
using _Parent = _MFCall<_Sign...>;

//从_Fn构造
template<class _Fx>
void _Assign(_Fx&& _Fn) {
using _Dx = std::decay_t<_Fx>;
using _OrDx = _Parent::template _Origial<_Dx>;
static_assert(std::is_constructible_v<_Dx, _Fx>, "Function type wrong, try std::move?");
//先销毁自身
this->destory();

this->template construct<_Fx, _Dx, _OrDx>(std::forward<_Fx>(_Fn));
}

public:
//接入的函数需要进行检查,首先是需要能够从_Fx构造到_Dx
template<class _Fx>
mfunction(_Fx&& _Fn) {
using _Dx = std::decay_t<_Fx>;
using _OrDx = _Parent::template _Origial<_Dx>;
static_assert(std::is_constructible_v<_Dx, _Fx>, "Function type wrong, try std::move?");
static_assert(_Parent::template _Invocable<_OrDx>, "Callable signature does't match _Rx(_Ax...)");
this->template construct<_Fx, _Dx, _OrDx>(std::forward<_Fx>(_Fn));
}

mfunction() {
}

mfunction& operator=(std::nullptr_t) noexcept{
this->destory();
return *this;
}
//禁止复制
mfunction(const mfunction&) = delete;
mfunction& operator=(const mfunction&) = delete;

//_Move函数不保证move noexcept
mfunction(mfunction&& _Other) {
_Parent::move_from(std::move(_Other.pack));
}

mfunction& operator=(mfunction&& _Other) {
if (this != std::addressof(_Other)) {
_Parent::assign(std::move(_Other));
}
return *this;
}

//会检查是否处于empty状态
~mfunction() {
this->destory();
}

using _Parent::operator();
using _Parent::empty;
using _Parent::assign;
};

}
}

#endif

第二层萃取 const noexcept &|&&的没有实现,但是原理都是一样的。

性能测试

横向对比了std::move_only_functionstd::function的性能,结果如下:

测试项 (对象类型) 操作 mfunction std::move_only_function std::function 你的排名 / 表现分析
SmallTrivial (小对象+平凡) 构造/析构 0.65 0.62 1.23 🥈 极其逼近 MSVC 标准库
调用 (Invoke) 1.24 1.05 1.04 🥉 存在瓶颈 (~0.2ns 差距)
移动构造 106 104 108 🥈 几乎无差异
移动赋值 106 103 105 🥈 几乎无差异
SmallNonTrivial (小对象+非平凡) 构造/析构 0.63 1.04 1.25 🥇 全场最快!反超标准库!
调用 (Invoke) 1.3 1.04 1.03 🥉 存在瓶颈 (~0.25ns 差距)
移动构造 105 105 104 🥈 误差范围内,并列第一
移动赋值 106 106 105 🥈 误差范围内,并列第一
LargeTrivial (大对象堆分配) 构造/析构 21.5 21.1 22.3 🥈 堆分配(new/delete)开销,符合预期
调用 (Invoke) 1.26 1.03 1.03 🥉 存在瓶颈 (~0.23ns 差距)
移动构造 114 114 115 🥇 堆指针互换,极致速度
移动赋值 121 127 126 🥇 指针互换,略优于标准库
LargeNonTrivial (大对象+非平凡) 构造/析构 21.6 21.3 21.8 🥈 完全符合预期
调用 (Invoke) 1.23 1.04 1.03 🥉 存在瓶颈 (~0.2ns 差距)

分析下来可以看见本应该会被std::move_only_function拉开差距的trivial优化反而没啥太明显的地方,这是因为/O2优化的结果。

最大的瓶颈就是在Invoke上,出现了if分支导致CPU流水线中引入了一个跳转分支,虽然有分支预测技术,但是判空的汇编代码开销也不少。标准库中使用的技术是零分支技术,同时使用了哑虚表技巧,这样就避免了虚表为空,同时调用“空状态”虚表invoke时也会报错。