源码剖析之标准库
自从专心学习了模板的知识后,发现自己打开了一个新世界,就是可以进入到标准库的源码世界,会觉得自己的水平和视野提升了很多。对很多类型了解了后,用起来就更加得心应手了。
C++11
std::tuple
先介绍tuple,因为这是实现function/bind的基础结构。其实本质就是模板+继承实现的
template<>
class tuple;
template<>
class tuple<>{};
template<class _This, class... _Rest>
class tuple<_This,_Rest>:private tuple<Rest...>{
_Tuple_val<_This> _Myfirst;
};
有趣的是这里使用了私有继承的方式,看起来好像是每一层继承都只能控制自己内部的变量。
std::bind
MSVC的实现是通过_Binder
模板来实现的。然后通过这个_Binder
的对象,移动构造了std::function
对象。这个模板有两种实现,区别在于是否强制要求来返回值类型,通过一个_Unforced的标签类来区分的。
template<class _Ret, class _Fx, class... _Types>
class _Binder: public _Binder_result_type<_Ret, _Fx>::type {
private:
using _Seq = index_sequece_for<_Types...>;
using _First = decay_t<_Fx>;
using _Second = tuple<decay_t<_Types>…>;
_Compressed_pair<_First, _Second> _Mypair;
public:
constexpr explicit _Binder(_Fx&& _Func, _Types&&... _Args)
: _Mypair(_One_then_variadic_args_t{}, _STD forward<_Fx>(_Func), _STD forward<_Types>(_Args)…){ }
};
std::bind的使用很简单,绑定和调用。因此这个函数主要关注构造函数和调用运算符。我们先来看下构造函数,其实就是初始化了一个_Compressed_pair,但是传了一个标签类,目的是为了调用不同的构造函数。其实这个_Compressed_pair类型是看第一个参数是否为空,如果为空则该类型被继承,否则作为成员对象,因此获得类型第一个的对象指针,需要通过_Get_first
函数来实现,获得第二个成员则直接用_Myval2
即可,因为永远都是通过成员对象来实现的。
用_Compressd_pair存储来可调用对象和对应的绑定值,这些绑定值是通过std::tuple来存储下来的。但是有些是绑定值是std::placeholders空间下的,因此需要利用std::tuple的功能,在调用生成的_Binder<_Ret, _Fx, _Types...>
的调用运算符时,可以找到对应位置去替换成真实的值。
从这里我们也知道了,std::function的初始化是接受了一个_Binder<_Ret, _Fx, _Types...>
可调用对象的。
这里有个疑问是为什么_Fx
需要退化掉呢?
当使用该可调用对象时,会重新传入还未绑定的真实值,这时候真实值要去替换tuple中的假的值,因此需要Fix一下,msvc的实现中利用了_Select_fixer
类型来对四种fixer实现了_Fix
函数
_Fix_arg(){
return _Select_fixer<_Cv_TiD>::_Fix(_Tid, move(_Ut));
}
return _Call(_Obj, _Fix_arg(get<_Ix>(_Tpl),move(_Ut)) … );
这四种类型是
- reference wrapper fixer
- nested bind fixer
- identity fixer
- placeholder fixer
这里的
_Select_fixer
通过四种模版的偏特化,对应来四种的实现,每一种中提供了静态成员函数_Fix
来实现对应的功能。
可以注意到的是,对于最后一种占位符的fixer其实就是通过std::tuple::get模板来把新传入的真实值组成的tuple中取出真实值来去作为参数传递。
std::function
该类型是利用了小对象优化的,那么多小的对象是小对象呢,在MSVC的头文件
constexpr int _Small_object_num_ptrs = 6+16/sizeof(void*);
这个定义的意思是在32位机器上是10个字节,在64为机器上是8个字节。
union _Storage{
max_align_t _Dummy1;
char _Dummy2[_Small_object_num_ptrs];
_Ptrt* _Ptrs[_Small_object_num_ptrs];
};
从这里可以看到_Ptrs是最大的数组,因此这个union的最大大小是8*8=64字节,但是实际上我们只用一个指针那就是_Ptrs[_Small_object_num_ptrs-1]
,因为函数对象其实就是调用那个函数指针而已。如果超过64字节,则需要在堆上分配通过new运算符来分配了。
template<class _Ret, class... _Types>
class _Func_class: public _Arg_types<_Types...>{
public:
using _Ptrt = _Fun_base<_Ret, _Types...>;
};
这里的_Arg_types应该是老的兼容代码,如果参数个数超过2个,那么该类型其实是个空类型。这里的类型_Fun_base
非常的关键,其实是利用了动态多态的方式,这个类型是抽象类,本质只是提供了一些接口。
template<class _Rx, class... _Types>
class _Func_base{
public:
virtual _Fun_base* _Copy() const =0;
virtual _Fun_base* _Move() noexcept =0;
virtual _Rx _Do_call() =0;
private:
virtual const void* _Get() const noexcept =0;
};
template<class _Alloc, class _Value_type>
using _Rebind_alloc_t = typename allocator_traits<_Alloc>::template rebind_alloc<_Value_type>;
子类有两个
_Func_impl
_Func_impl_no_alloc
区别是是否使用了分配器,如果使用了分配器_Alloc则要用_Compressed_pair<_Alloc, _Callable>
的对象作为成员。这两种区分是因为std::function有构造函数是支持空间分配器的。这里的_Rebind_alloc_t
其实就是得到确定的内存分配器,既是_Alloc<_Value_type>
,写的复杂了一些。
通过这样的实现,既可以绑定函数指针,也可以绑定可调用对象,如果是可调用对象则需要用分配器来实现。就是通过指针+虚函数实现了动态绑定。
//TODO:有个不太理解的地方:由于可调用对象可能带有其他成员,因此大小是未知的,所以需要有小对象优化?比如lambda本质应该是个函数对象,捕获值会作为类型成员存储起来。
因此std::function的主要陷阱是防止用太大的可调用对象初始化导致需要通过new运算符来进行,如果绑定的是函数指针则不会有这些问题,或者不要在绑定较大可调用对象的情况下频繁去切换。
std::reference_wrapper
主要是用来构造函数对象的时候,能够保留函数对象中某些参数的引用特性。
由于引用是不能够被存储的,因此其实本质上就是用指针来存储的。既可以包装对象,又可以包装函数。
由于std::thread和std::bind内部都是有个std:tuple来存储可调用对象的参数表对应的所有对象的,初始化std::tuple的时候不能用引用来初始化,因此需要用std::reference_wrapper来包装一下。
当_Fix
的时候,就通过_Select_fixer
类模板的静态成员函数_Fix
来重新拿到该类型的引用。重新拿引用是通过函数调用的时候,返回该类型的引用来实现的。
template <class _Cv_TiD>
struct _Select_fixer<_Cv_Tid, true, false, 0> {
template <class _Untuple>
static constexpr auto _Fix(_Cv_Tid& _Tid, _Untuple&&) noexcept -> typename _Cv_TiD::type& {
return _Tid.get();
}
};
可以看到_Cv_TiD
其实就是std::reference_wrapper
类型,有个特征type就拿到了对应的类型。
std::invoke
std::shared_ptr/std::weak_ptr
看的是MSVC的STL实现,基类是_Ptr_base,这个基类是std::shared_ptr和std::weak_ptr共同的基类。
有两个数据成员
element_type* _Ptr
,裸指针本身_Ref_count_base* _Rep
,引用计数对象
所以sizeof(std::shared_ptr)=16,每个指针都是8个字节。 有意思的事情是,这里的_Rep指向的对象是在make_shared中产生的,通过调用_Ref_count_bounded_array的构造函数产生的,然后再拷贝构造过去的。而且拷贝构造是通过swap来实现的。
class _Ref_count_bounded_array: public _Ref_count_base{};
_Ref_count_base
这个类型有两个成员都是_Atomic_counter_t类型的,一个是_Uses,另一个是_Weaks。_Uses是引用计数,_Weaks是弱引用的计数,且当弱引用计数为0的时候,也会调用_Delete_this函数。
在文件xatomic.h文件中有如下定义:
using _Atomic_counter_t = unsigned long
对于_Uses的增加是通过宏来实现的
#define _INTRIN_RELAXED(x) x
#define _MT_INCR(x) _INTRIN_RELAXED(_InterlockedIncrement)(reinterpret_cast<volatile long*>(&x))
我们再到intrin0.h中看到_InterlockedIncrement有两个重载函数
__MACHINE(long __MACHINECALL_CDECL_OR_DEFAULT _InterlockedIncrement(long volatile* _Addend))
__MACHINEWVMPURE(long _InterlockedIncrement(long volatile* _Addend))
这里的_InterlockedIncrement是原子性增减,在同一个进程中实现线程同步,线程之间的互锁函数。所以对于引用计数的_Uses成员的操作是原子性的,但是要注意的是,对于智能指针指向的对象来说不是线程安全的。
std::shared_ptr
std::shared_ptr继承自_Ptr_base且没有新增数据成员,除此之外的成员包括
- operator =
- reset
- swap
- unique
std::weak_ptr
std::weak_ptr也是继承自_Ptr_base且没有新增数据成员,函数成员包括
- operator =
- reset
- swap
- lock
- expired
有意思的是lock操作,函数如下
shared_ptr<_Ty> lock() const noexcept
{
shared_ptr<_Ty> _Ret;
(void) _Ret._Construct_from_weak(*this);
return _Ret;
}
这里的_Construct_from_weak来自_Ptr_base的函数。
std::unique_ptr
unique_ptr并不是从_Ptr_base中派生过来的。且其声明是接受两个模板参数的。
template<class _Ty,class _Dx>
class unique_ptr<_Ty[],_Dx>
{
...
private:
_Compressed_pair<_Dx,pointer> _Mypair;
};
template<class _Ty1,class _Ty2,bool=is_empty_v<_Ty1> && !is_final_v<_Ty1>>
class _Compressed_pair final: private _Ty1
{
_Ty2 _Myval2;
}
template<class _Ty1, class _Ty2>
class _Compressed_pair<_Ty1, _Ty2, false>
{
public:
_Ty1 _Myval1;
_Ty2 _Myval2;
}
可以看到_Compressed_pair是继承自元素对应的类型,并且有个数据成员是指针。且有个特化版本,如果元素对应的类型非空,且是不能够派生的类型则有特化版本的类型,存储了一对数据成员,也就是unique_ptr中的_Compressed_pair<_Dx,pointer>即是删除器和指针本身。这么设计的原因是,如果元素是空的,那么是不需要删除器的。使用模板的原因就是为了实现这种特殊情况,需要类型计算才知道。
std::iostream/std:fstream
主要是ios_base/basic_ios和basic_streambuf类型。
std::ios_base
最基础的ios_base类仅有私有成员,主要处理IO状态,IO异常,IO格式化,IO事件,本地化等。
class ios_base: public _Iosb<int> {
public:
using fmtflags = int;
using iostate = int;
using openmode = int;
using seekdir = int;
using event_callback = void(__CLRCALL_OR_CDECL*)(event, ios_base&, int);
private:
struct _Iosarray : _Crt_new_delete { // list element for open-ended sparse array of longs/pointers
public:
__CLR_OR_THIS_CALL _Iosarray(int _Idx, _Iosarray* _Link)
: _Next(_Link), _Index(_Idx), _Lo(0), _Vp(nullptr) {} // construct node for index _Idx and link it in
_Iosarray* _Next; // pointer to next node
int _Index; // index of this node
long _Lo; // stored long value
void* _Vp; // stored pointer value
};
struct _Fnarray : _Crt_new_delete { // list element for open-ended sparse array of event handlers
__CLR_OR_THIS_CALL _Fnarray(int _Idx, event_callback _Pnew, _Fnarray* _Link)
: _Next(_Link), _Index(_Idx), _Pfn(_Pnew) {} // construct node for index _Idx and link it in
_Fnarray* _Next; // pointer to next node
int _Index; // index of this node
event_callback _Pfn; // pointer to event handler
};
iostate _Mystate; // stream state
iostate _Except; // exception mask
fmtflags _Fmtfl; // format flags
streamsize _Prec; // field precision
streamsize _Wide; // field width
_Iosarray* _Arr{nullptr}; // pointer to first node of long/pointer array
_Fnarray* _Calls{nullptr}; // pointer to first node of call list
locale* _Ploc{nullptr}; // pointer to locale
__PURE_APPDOMAIN_GLOBAL static int _Index;
__PURE_APPDOMAIN_GLOBAL static bool _Sync;
};
这里有个_Iosb
的模板类,本质上是提供了非常多的编译期的值。从值特征可以发现,前五个成员本质上都是int类型标志位。有两个指针成员,对应的其实是链表,一个应该是用于buffer指针,另一个是用于事件回调的。
可以发现ios_base并不是一个模板类,而是通过提供一些成员和编译期值来辅助构造子类basic_ios的。
std::basic_ios
basic_ios是basic_istream和basic_ostream的基类。
template <class _Elem, class _Traits>
class basic_ios : public ios_base { // base class for basic_istream/basic_ostream
public:
using _Myos = basic_ostream<_Elem, _Traits>;
using _Mysb = basic_streambuf<_Elem, _Traits>;
private:
_Mysb* _Mystrbuf; // pointer to stream buffer
_Myos* _Tiestr; // pointer to tied output stream
_Elem _Fillch; // the fill character
};
这里的成员_Mystrbuf
很关键,这里才是真正对应的buffer。
template <class _Elem, class _Traits>
class basic_istream : virtual public basic_ios<_Elem, _Traits> { // control extractions from a stream buffer
private:
streamsize _Chcount; // the character count
};
basic_istream只有一个记录读取次数的值。本质上这个类都是要通过_Mystrbuf
来操作的,也就是std::basic_streambuf。
std::basic_streambuf
还有个基础类型是basic_streambuf,是作为缓冲区。
template <class _Elem, class _Traits>
class basic_streambuf { // control read/write buffers
private:
_Elem* _Gfirst; // beginning of read buffer
_Elem* _Pfirst; // beginning of write buffer
_Elem** _IGfirst; // pointer to beginning of read buffer
_Elem** _IPfirst; // pointer to beginning of write buffer
_Elem* _Gnext; // current position in read buffer
_Elem* _Pnext; // current position in write buffer
_Elem** _IGnext; // pointer to current position in read buffer
_Elem** _IPnext; // pointer to current position in write buffer
int _Gcount; // length of read buffer
int _Pcount; // length of write buffer
int* _IGcount; // pointer to length of read buffer
int* _IPcount; // pointer to length of write buffer
protected:
locale* _Plocale; // pointer to imbued locale object
};
这个类都有个很特别的成员,就是locale。这个类比较复杂,主要成员就是_Locimp* _Ptr
,目的是定义一些facet可以对元素进行一些解析和格式操作。
basic_istream和basic_ostream两种类型都有相应的迭代器
- istream_iterator/ostream_iterator
- istreambuf_iterator/ostreambuf_iterator
利用迭代器执行一些操作很方便。
std::basic_filebuf
template <class _Elem, class _Traits>
class basic_filebuf : public basic_streambuf<_Elem, _Traits> { // stream buffer associated with a C stream
private:
const _Cvt* _Pcvt; // pointer to codecvt facet (may be null)
_Elem _Mychar; // putback character, when _Ungetc fails
bool _Wrotesome; // true if homing sequence may be needed
typename _Traits::state_type _State; // current conversion state
bool _Closef; // true if C stream must be closed
FILE* _Myfile; // pointer to C stream
_Elem* _Set_eback; // saves eback() during one-element putback
_Elem* _Set_egptr; // saves egptr()
};
std::basic_fstream
template <class _Elem, class _Traits>
class basic_fstream : public basic_iostream<_Elem, _Traits> { // input/output stream associated with a C stream
public:
using _Myfb = basic_filebuf<_Elem, _Traits>;
private:
_Myfb _Filebuffer;
};
可以看到原来fstream是基于iostream来的,相应的basic_filebuf是继承自basic_streambuf。
std::mutex/std::condition_variable
其实这两者都是要有操作系统支持的,通过syscall来实现的,所以实现本身是很简单的,都是调用的系统调用实现。
std::promise
我们先看看整个的继承体系吧。
template<class _Ty>
class promise {
private:
_Promise<_Ty> _MyPromise;
};
template<class _Ty>
class _Promise {
private:
_State_manager<_Ty> _State;
bool _Future_retrieved;
};
这里有个成员_MyPromise,他的构造函数要求_Associated_state
,其实这个类型用来初始化了_State_manager
的对象。
template <class _Ty>
class _State_manager{
private:
_Associated_state<_Ty>* _Assoc_state;
bool _Get_only_once;
};
template <class _Ty>
class _Associated_state {
private:
_Atomic_counter_t _Refs;
public:
conditional_t<is_default_constructible_v<_Ty>, _Ty, _Result_holder<_Ty>> _Result;
mutex _Mtx;
condition_variable _Cond;
};
这里有个_Result
是用来存储promise保证未来会设置的值。这里为什么_Associated_state需要用到mutex和condition_variable呢
- mutex是用于promise可能用于多个线程,所以加锁
- condition_variable是因为set_value的时候要通知不同线程中的future调用的get操作
- 有个
_Refs
记录引用计数,是因为可能有多个future都依赖于这个关联状态对象
而future/shared_future的实现本身非常的简单
template <class _Ty>
class future : public _State_manager<_Ty> {
};
template <class _Ty>
class shared_future : public _State_manager<_Ty> {
};
可以看到future和shared_future本身是没有任何数据成员的,完全是把_State_manager
给包了一下,主要是提供了一个get函数
_Ty future::get() {
// block until ready then return the stored result or throw the stored exception
future _Local{_STD move(*this)};
return _STD move(_Local._Get_value());
}
const _Ty& shared_future::get() const {
// block until ready then return the stored result or throw the stored exception
return this->_Get_value();
}
可以看到future::get()
是通过移动构造来实现的。移动构造过程中保证了_Associated_state
只会有一份存在,这一份是构造promise时候才会new出来的,因此如果是future的话,promise里面的关联状态在future::get
调用后是残缺的无法继续使用。但是shared_future::get()
并不会破坏关联状态。
promise的意义还在于,能够从线程中返回值。传统的thread构造,只能设置join/detach,但是promise可以设置set_value_at_thread_exit非常的方便,也是构造packaged_task的工具。
std::async
std::packaged_task
C++14
std::integer_sequence
这个成员和std::integer_constant都是用来辅助编译期计算用的。这个类型本身并没有给出太多的成员,所以有点类似于标签类,是用于编译期多态,匹配多种不同参数表的相同名称的函数用的。
template <class _Ty, _Ty... _Vals>
struct integer_sequence { // sequence of integer parameters
static_assert(is_integral_v<_Ty>, "integer_sequence<T, I...> requires T to be an integral type.");
using value_type = _Ty;
_NODISCARD static constexpr size_t size() noexcept {
return sizeof...(_Vals);
}
};
本身只提供了一个size成员函数。
template <size_t... _Vals>
using index_sequence = integer_sequence<size_t, _Vals...>;
通常都是使用index_sequence。
std::exchange/std::swap
std::exchange本身只是通过两次移动构造帮助实现交换对象。 std::swap的目的是想通过ADL规则,如果用户没有定义自己的swap,则使用标准库中的std::swap。
std::shared_lock/std::unique_lock/std::scoped_lock
C++17
std::variant/std::visit
先来看看定义
template<class... _Types>
class variant: private _SMF_control<_Variant_destory_layer<_Types...>, _Types...>{};
注意到是,这个最外层子类定义是没有数据成员的,只是提供了emplace接口而已,真正的实现都在基类中,而这个_SMF_control
是通过判断_Types
中类型的特殊成员函数的特征来去初始化不同的特殊成员函数的,主要考虑了拷贝/移动的赋值运算符是否删除。所以真正的内部实现,是在_Variant_destory_layer
里面的,而这个类型又是继承自_Variant_base
类型,增加一层抽象的主要目的是如果析构函数是非简单的,那么需要在析构函数中调用this->_Destory()
。
我猜测SMF应该是Special Member Function的意思。
template<class... _Types>
class _Variant_base : private _Variant_storage<Types...> {
public:
using _Index_t = _Variant_index_t<sizeof...(_Types)>;
_Index_t _Which;
};
在_Variant_base
层级主要是实现了对Variant
的特殊成员函数的支持,比如有_Construct_from
和_Assign_from
等内部函数,用于支撑上层的构造函数和拷贝构造函数等。这里有个_Which
成员指示目前是存储的参数包中第几个类型。因此真正对数据的存储是写在_Variant_storage
里面的,其实现如下
template<bool _TrivalDestruction, class... _Types>
class _Variant_storage_ {};
template<class _Frist, class... _Rest>
class _Variant_storage_<true, _First, _Rest...>{
public:
union{
remove_const_t<_First> _Head;
_Variant_storage<_Rest...> _Tail;
};
template<class... _Types>
constexpr explicit _Variant_storage_(integral_const<size_t, 0>, _Types&&... _Args)
: _Head(static_cast<_Types>(_Args)…){}
template<size_t _Idx, class... _Types, enable_if_t<(_Idx>0)> = 0>
constexpr explicit _Variant_storage_(integral_const<size_t, _Idx>, _Types&&... _Args)
: _Tail(integral_const<size_t, _Idx-1>, static_cast<_Types&&>(_Args)…){}
};
我们可以发现,根据序列的不同去初始化了不同的成员_Head
或者_Tail
,而这里的_Idx
值就是来自_Variant_base
这一层的_Which
成员。
整体的设计很有层次感
variant
提供接口_Variant_base
存储了序列值,指向了第几个类型,同时支持了拷贝构造和移动构造的实现_Variant_storage_
通过两个构造函数初始化union的不同成员,实现了对不同类型数据的存储 因此我们知道,每次重新初始化std::variant
的时候,本质上会重新构造_Variant_storage_
,而且由于需要支持运行时的重新构造,会导致重新赋值多少次就会初始化多少次_Variant_storage_
的类型。
我们再来看配套的std::visit
的实现。其实C++17中就有不少类似mp11中的类型表等等概念,比如_Meta_list
和_Meta_as_list
,后者是把类模板模板(TTP)转化为_Meta_list
。还有_Meta_cartesian_product
是用两个元函数连续对类型表进行处理。
_Meta_list
_Meta_as_list
_Meta_transform
_Meta_quote
_Meta_as_integer_sequence
_Meta_cartesian_product
利用mixin来实现调用std::visit
是一种有趣的方法
template<typename... Ts>
struct overload: Ts...{
using Ts::operator()...;
};
template<typename... Ts>
overload(Ts...) -> overload<Ts...>;
std::optional
和std::visit
的最基本原理是很像的,都是通过union和初始化不同union的构造函数来实现的,且都因为union的原因需要考虑析构函数,这也是原生union的不足。
std::any
存储方式也是利用了union但是比较复杂。
std::apply
C++20
ranges
coroutines
演讲
Deducing this
演讲的名字是A Journey Into Non-Virtual Polymorphism in C++ - Rudyard Merriam - CppCon 2023
,这里面提到的问题是,对于多个毫不相关没有继承关系的类型,如何实现用相同的接口来处理。给出的三种方案是std::any,std::variant/std::visit和std::tuple/std::apply。但是最终方案是c++23中的deducing this,是对于CRTP的更好的实现,因为基类是相同的了。
// CRTP
template<typename T>
struct Shape{
void Draw(){Derived().Draw_impl();}
};
// deducing this
sturct Shape{
template<typename Self>
void Draw(this Self& self){self.Draw();};
};
小结
通过对这些标准库中的设施的研究,掌握了一些技巧
- 分层的设计,顶层实现对外接口;中间层实现元函数功能;底层支撑不同特化或者偏特化的实现
- union加上构造函数模板,由于union的存在需要考虑动态的析构函数