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中间层,这一层处理了noexcept和const还有&|&&修饰符,因此支持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调用的invoke和move_to是CallableImpl里面的,这里也是实现了类型擦除。不过,MSVC采用手搓虚函数表还是有其道理的:
- 减少RTTI数据,如果使用
virtual的类,编译器会为其生成RTTI数据,还有type_info,dynamic_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字节):
一开始看到可能会有点疑惑,明明_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; }
|
回到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 if constexpr (alignof(_Vt) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) { _Ptr = ::operator new(sizeof(_Vt), align_val_t{alignof(_Vt)}); } else #endif { _Ptr = ::operator new(sizeof(_Vt)); }
|
大小要求
这个条件很好理解,如果栈空间塞不下去,那么就需要在堆中申请空间。
在_Move_only_function_data联合体对象里,实现了一些模板变量,用于计算可用的空间
1 2 3 4 5 6 7 8 9 10 11
| template <class _Fn> static constexpr size_t _Buf_size = sizeof(_Pointers) - _Buf_offset<_Fn>;
template <class _Fn> static constexpr size_t _Buf_offset = alignof(_Fn) <= sizeof(_Impl) ? sizeof(_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>(); 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{}; 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>) { 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 { _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>) { 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处理的是缓存对象的移动。
assign:move_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&&...); void(*_Move)(SBO&,SBO&); void(*_Destory)(SBO&)noexcept; }
|
虚表处理的只是_SBO_Storage的调用、移动和析构操作。
虚表中需要处理的是如何原地构造,并且需要注意noexcept,还有over_align过度对齐类型。
_Invoke,需要提供_Dx,_Rx,_Ax,是否Noexcept,还有_OrDx,要实现限定符保留,必须使用_OrDx对函数对象强制类型转换static_cast<_OrDx>(*ptr)
_Move,移动src到dest,会调用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 layer,caller定义了仿函数,函数就带上了上述的修饰符。这个会影响虚表_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;
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; }*vtab = nullptr;
};
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)...); } }
template<class _Rx, class... _Ax> void _Move_heap(typename _MFPack<_Rx, _Ax...>::_sbo_storage& _dest, typename _MFPack<_Rx, _Ax...>::_sbo_storage& _src) { memcpy(&_dest.data, &_src.data, sizeof(typename _MFPack<_Rx, _Ax...>::_sbo_storage)); }
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...>; table._Move = _Move_heap<_Rx, _Ax...>; 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; } void destory() noexcept{ if (!empty()) { pack.vtab->_Destory(pack.storage); pack.vtab = nullptr; } } void move_from(_MyPack&& _other) { if (_other.vtab != nullptr) { _other.vtab->_Move(pack.storage, _other.storage); pack.vtab = _other.vtab; _other.vtab = nullptr; } } void assign(_MFBase&& _other) { if (!empty()) { this->destory(); } move_from(std::move(_other.pack)); } 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)); } pack.vtab = const_cast<_MyTable*>(_get_vtable_ptr<_Dx, _OrDx>()); }
bool empty() const noexcept{ return pack.vtab == nullptr; } 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"); };
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...>;
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: 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;
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; }
~mfunction() { this->destory(); }
using _Parent::operator(); using _Parent::empty; using _Parent::assign; };
} }
#endif
|
第二层萃取 const noexcept &|&&的没有实现,但是原理都是一样的。
性能测试
横向对比了std::move_only_function和std::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时也会报错。