发表于

源码剖析之标准库

自从专心学习了模板的知识后,发现自己打开了一个新世界,就是可以进入到标准库的源码世界,会觉得自己的水平和视野提升了很多。对很多类型了解了后,用起来就更加得心应手了。

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的存在需要考虑动态的析构函数