c++11 make_shared深度研究,从一个函数看仿变长参数模板实现方式

         本文基于vs2012中的std::make_shared函数进行分析,选取原因有两个,一是现在公司再用vs2012版本进行开发,我也就自然根据这个编译器进行的研究,第二是vs2012中并没有支持变长参数模板这个C++11的特性,但是make_shared仍然神奇的支持着“变长参数”,这就说明起码vs2012中的make_shared还没有用到变长参数就实现了这个功能,并且也导致一个问题,就是vs2013版本之后make_shared的实现方式可能发生了改变。所以在开始分析前,我要强调一下我所用的代码版本。

         首先我认为还是要介绍一下make_shared这个函数,虽然可能对于大部分人来说这个根本没用,如果觉得没用可以直接跳过这个废话阶段。make_shared是C++11中创建shared_ptr这种智能指针的一种方式,并且实际上很推荐使用这种方式去创建一个智能指针,而不是直接通过原生指针去赋值或者通过shared_ptr的构造函数去创建一个智能指针。因为那样的操作略微存在一些内存泄漏上的风险,具体问题请自行百度shared_ptr。make_shared函数的一般使用方式是auto smart_ptr = std::make_shared<PTR_TYPE>(PARAM_TYPE1param1, PARAM_TYPE2 param2,...);参数就是指针实例化时构造函数需要的参数。而经过试验会发现这个参数数量最大是5,当然理由会在后面说明。

         那接下来就正式开始分析这个函数。

         首先通过跳转找到这个函数的定义,会在memory中看到这样一段代码

#define_ALLOCATE_MAKE_SHARED( \

    TEMPLATE_LIST, PADDING_LIST,LIST, COMMA, X1, X2, X3, X4) \

template<class _Ty COMMA LIST(_CLASS_TYPE)> inline \

    shared_ptr<_Ty>make_shared(LIST(_TYPE_REFREF_ARG)) \

    {   /* make a shared_ptr */ \

    _Ref_count_obj<_Ty> *_Rx =\

        new _Ref_count_obj<_Ty>(LIST(_FORWARD_ARG)); \

    shared_ptr<_Ty> _Ret; \

    _Ret._Resetp0(_Rx->_Getptr(),_Rx); \

    return (_Ret); \

    } \

template<class _Ty, \

    class _Alloc COMMA LIST(_CLASS_TYPE)> inline \

    shared_ptr<_Ty>allocate_shared( \

        const _Alloc& _Al_arg COMMA LIST(_TYPE_REFREF_ARG)) \

    {   /* make a shared_ptr */ \

    typedef _Ref_count_obj_alloc<_Ty,_Alloc> _Refoa; \

    typename _Alloc::template rebind<_Refoa>::other _Alref =_Al_arg; \

    _Refoa *_Rx = _Alref.allocate(1);\

    _TRY_BEGIN \

        ::new (_Rx) _Refoa(_Al_arg COMMA LIST(_FORWARD_ARG)); \

    _CATCH_ALL \

        _Alref.deallocate(_Rx, 1); \

    _RERAISE; \

    _CATCH_END \

    shared_ptr<_Ty> _Ret; \

    _Ret._Resetp0(_Rx->_Getptr(),_Rx); \

    return (_Ret); \

    }

 

_VARIADIC_EXPAND_0X(_ALLOCATE_MAKE_SHARED, , , , )

#undef _ALLOCATE_MAKE_SHARED

这一段代码中可以找到长得很像make_shared的函数的定义,没错,那东西就是make_shared的定义。但是,这种代码,who understand?整个的函数定义都是包在一个宏里的,而这个宏的运行方式,可以用鬼畜来形容。这个定义实际上并不是c++11里的变长参数的实现方式,而是通过宏元编程,实现的代码生成,然后又通过代码生成,重载了6个make_shared函数。这6个函数分别接受0到5个参数,这也就是之前提到的最多接受5个参数的原因。

从结论上来讲,make_shared的定义是通过宏元实现的代码生成而来的重载函数,那么这个定义到底是如何产生的?其实如果熟悉宏的操作的话,会很清楚的了解到这个定义整个是包在一个_ALLOCATE_MAKE_SHARED的宏里的,而这个宏在最后_VARIADIC_EXPAND_0X(_ALLOCATE_MAKE_SHARED, , , , )的地方被使用过一次,之后就卸载掉了,所以第一把钥匙就是_VARIADIC_EXPAND_0X这个宏。跳转过去的话,会看到这个宏的定义是这样的

#define_VARIADIC_EXPAND_0X(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_0(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_1X(FUNC, X1, X2,X3, X4)

我们直接跳转,看一下新出现的这两个宏又是什么东西

#define_VARIADIC_EXPAND_0(FUNC, X1, X2, X3, X4) \

FUNC(_TEM_LIST0, _PAD_LIST0, _RAW_LIST0, , X1, X2, X3, X4)

 

#define_VARIADIC_EXPAND_1X(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_1(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_2X(FUNC, X1, X2, X3, X4)

变得越来越复杂了。这里暂时不去接着打开_VARIADIC_EXPAND_1X宏,稍微研究一下_VARIADIC_EXPAND_0这个宏是什么东西。FUNC是宏接受的一个参数,后面的X1,X2,X3,X4也是宏自己接受的参数,而_TEM_LIST0, _PAD_LIST0, _RAW_LIST0这三个东西又是三个宏,而且是让人完全摸不着头脑的宏。这里我们也把它放上来

#define_TEM_LIST0(MAP)

 

#define_PAD_LIST0  _PAD_LIST0_5

这个宏继续展开

#define_PAD_LIST0_5(MAP) \

    MAP(0) _COMMA MAP(1) _COMMA MAP(2) _COMMA MAP(3) _COMMA \

    MAP(4) _COMMA MAP(5)

这里有两个奇怪的东西,MAP(NUM)_COMMAMAP(NUM)是打不开的,它实际上也不是一个宏,而_COMMA实际上就是用宏包起来的一个“,”,定义在下面。并且请记住这个宏,在很多地方都有用过,可以自动把它脑补成一个逗号

#define_COMMA  ,   /* for commas in macro parameters */

 

#define_RAW_LIST0(MAP)

 

这几个宏让人完全看不懂,但是先把他放到一边,再来展开一下_VARIADIC_EXPAND_1X这个宏看一下:

#define_VARIADIC_EXPAND_1(FUNC, X1, X2, X3, X4) \

FUNC(_TEM_LIST1, _PAD_LIST1, _RAW_LIST1, _COMMA, X1, X2, X3, X4)

 

#define_VARIADIC_EXPAND_2X_VARIADIC_EXPAND_25

继续展开

#define_VARIADIC_EXPAND_25(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_2(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_3(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_4(FUNC, X1, X2, X3, X4) \

    _VARIADIC_EXPAND_5(FUNC, X1, X2, X3, X4)

突然出现这么多的宏,没关系,继续展开

#define_VARIADIC_EXPAND_2(FUNC, X1, X2, X3, X4) \

FUNC(_TEM_LIST2,_PAD_LIST2, _RAW_LIST2, _COMMA, X1, X2, X3, X4)

 

#define_VARIADIC_EXPAND_3(FUNC, X1, X2, X3, X4) \

FUNC(_TEM_LIST3, _PAD_LIST3, _RAW_LIST3, _COMMA, X1, X2, X3, X4)

 

#define_VARIADIC_EXPAND_4(FUNC, X1, X2, X3, X4) \

FUNC(_TEM_LIST4, _PAD_LIST4, _RAW_LIST4, _COMMA, X1, X2, X3, X4)

 

#define_VARIADIC_EXPAND_5(FUNC, X1, X2, X3, X4) \

FUNC(_TEM_LIST5, _PAD_LIST5, _RAW_LIST5, _COMMA, X1, X2, X3, X4)

这样展开来应该就发现一些问题了。实际上在库文件里,这些代码也都是写在一起的。那么接下来,我们把_VARIADIC_EXPAND_0X这个宏重新展开,看一看这个东西究竟是什么东西

#define_VARIADIC_EXPAND_0X(FUNC, X1, X2, X3, X4) \

FUNC(_TEM_LIST0, _PAD_LIST0, _RAW_LIST0, , X1, X2, X3, X4)\

FUNC(_TEM_LIST1, _PAD_LIST1, _RAW_LIST1, _COMMA, X1, X2, X3, X4)\

FUNC(_TEM_LIST2, _PAD_LIST2, _RAW_LIST2, _COMMA, X1, X2, X3, X4) \

FUNC(_TEM_LIST3, _PAD_LIST3, _RAW_LIST3, _COMMA, X1, X2, X3, X4) \

FUNC(_TEM_LIST4, _PAD_LIST4, _RAW_LIST4, _COMMA, X1, X2, X3, X4) \

FUNC(_TEM_LIST5,_PAD_LIST5, _RAW_LIST5, _COMMA, X1, X2, X3, X4)

 

以上就是_VARIADIC_EXPAND_0X宏展开的形态。但是这个形态说实话还是很让人摸不着头脑的,接下来需要进一步的分析

还记得make_shared定义处的代码吗?原本的定义是这样子的_VARIADIC_EXPAND_0X(_ALLOCATE_MAKE_SHARED, , , , ) ,那么接下来我们要做一件事情,就是把_ALLOCATE_MAKE_SHARED带入到_VARIADIC_EXPAND_0X宏里,注意这里的X1,X2,X3,X4都是空的,也就是说这里并没有用到X1,X2,X3,X4,我们可以直接把这几个东西忽略掉。展开之后的效果是这样子的

_ALLOCATE_MAKE_SHARED (_TEM_LIST0, _PAD_LIST0, _RAW_LIST0, , , , , )\

_ALLOCATE_MAKE_SHARED (_TEM_LIST1, _PAD_LIST1, _RAW_LIST1, _COMMA, , , , ) \

_ALLOCATE_MAKE_SHARED (_TEM_LIST2, _PAD_LIST2, _RAW_LIST2, _COMMA, , , , ) \

_ALLOCATE_MAKE_SHARED (_TEM_LIST3, _PAD_LIST3, _RAW_LIST3, _COMMA, , , , ) \

_ALLOCATE_MAKE_SHARED (_TEM_LIST4, _PAD_LIST4, _RAW_LIST4, _COMMA, , , , ) \

_ALLOCATE_MAKE_SHARED (_TEM_LIST5, _PAD_LIST5, _RAW_LIST5, _COMMA, , , , )

 

后面的“\”这里可以省略掉了,因为是生成的代码,所以代码会抽搐到一行,但是为了看的方便我们还是会把它放在几行

 

然后接下来,我们要做的是把_VARIADIC_EXPAND_0X这个宏替换掉,这个宏还记得在哪里吗?就在make_shared的最上面,定义是这样的ALLOCATE_MAKE_SHARED( TEMPLATE_LIST, PADDING_LIST,LIST, COMMA, X1, X2, X3, X4) \同样为了观看方便,我把中间的“\”省去了。但是接下来,我们不会继续去展开这个宏,而是要分析一下这个宏本身。因为后面的allocate_shared函数可以暂时不用管,我们只分析包在这个宏里的make_shared函数

template<class _Ty COMMA/*这是逗号*/ LIST/*这是参数*/ (_CLASS_TYPE)> inline \

    shared_ptr<_Ty>make_shared(LIST/*这是参数*/ (_TYPE_REFREF_ARG)) \

    {   /* make a shared_ptr */ \

    _Ref_count_obj<_Ty> *_Rx =\

        new _Ref_count_obj<_Ty>(LIST/*这是参数*/ (_FORWARD_ARG)); \

    shared_ptr<_Ty> _Ret; \

    _Ret._Resetp0(_Rx->_Getptr(),_Rx); \

    return (_Ret); \

    } \

我在这个函数中加了几个注释,并且接下来我需要解释一个重要的内容,在上面展开_VARIADIC_EXPAND_0X的时候,请把所有的_ALLOCATE_MAKE_SHARED当做函数调用,请把所有的_ALLOCATE_MAKE_SHARED当做函数调用,重要的事情说三遍。然后在真正的_ALLOCATE_MAKE_SHARED定义中请把(TEMPLATE_LIST,PADDING_LIST, LIST, COMMA, X1, X2, X3, X4)全部当成函数参数,请把(TEMPLATE_LIST,PADDING_LIST, LIST, COMMA, X1, X2, X3, X4)全部当成函数参数,请把(TEMPLATE_LIST,PADDING_LIST, LIST, COMMA, X1, X2, X3, X4)全部当成函数参数,这一次真的说了三遍。这样一来,我上面添加的注释也就应该明白了。那么做一个对应,传给LIST的参数到底是什么?这时候就可以知道是_RAW_LIST0...5。之前已经展示过_RAW_LIST0的定义,现在我把05的定义都拿出来,看一下这是什么东西

#define_RAW_LIST0(MAP)

 

#define_RAW_LIST1(MAP) \

    MAP(0)

 

#define_RAW_LIST2(MAP) \

    MAP(0) _COMMA MAP(1)

 

#define_RAW_LIST3(MAP) \

    MAP(0) _COMMA MAP(1) _COMMA MAP(2)

 

#define_RAW_LIST4(MAP) \

    MAP(0) _COMMA MAP(1) _COMMA MAP(2) _COMMA MAP(3)

 

#define_RAW_LIST5(MAP) \

    MAP(0) _COMMA MAP(1) _COMMA MAP(2) _COMMA MAP(3) \

    _COMMA MAP(4)

 

到这里,可能有些人还是不明白这里面有什么玄机,所以我稍后再展开两段代码,在这两段代码中,我会直接把能换掉的都换掉。在此之前,其实还有三个宏我没有提到,就是在make_shared中的三个宏,这里我也把他们都展示出来,稍后一并展开

#define_CLASS_TYPE(NUM)    \

    class_VAR_TYPE(NUM)

继续展开

#define_VAR_TYPE(NUM)  \

    _V ## NUM ## _t

 

#define_TYPE_REFREF_ARG(NUM)   \

    _TYPE_REFREF(NUM) _VAR_VAL(NUM)

继续展开

#define_TYPE_REFREF(NUM)   \

    _VAR_TYPE(NUM)&&

 

#define_VAR_TYPE(NUM)  \

    _V ## NUM ## _t

 

#define_VAR_VAL(NUM)   \

    _V ## NUM

 

#define_FORWARD_ARG(NUM)   \

    _STD forward<_VAR_TYPE(NUM)>(_VAR_VAL(NUM))

 

然后我展开的两段代码,分别是0个参数的make_shared和一个参数的make_shared。这里的代换比较复杂,请自己努力思考

template<class _Ty , > inline

    shared_ptr<_Ty>make_shared()

    {   /* make a shared_ptr */

    _Ref_count_obj<_Ty> *_Rx =

        new _Ref_count_obj<_Ty>();

    shared_ptr<_Ty> _Ret;

    _Ret._Resetp0(_Rx->_Getptr(),_Rx);

    return (_Ret);

    }

 

template<class _Ty , _CLASS_TYPE(0)> inline

    shared_ptr<_Ty>make_shared(_TYPE_REFREF_ARG(0))

    {   /* make a shared_ptr */

    _Ref_count_obj<_Ty> *_Rx =

        new _Ref_count_obj<_Ty>( _FORWARD_ARG(0));

    shared_ptr<_Ty> _Ret;

    _Ret._Resetp0(_Rx->_Getptr(),_Rx);

    return (_Ret);

    }

继续替换

template<class _Ty , class_V0_t> inline

    shared_ptr<_Ty>make_shared(_V0_t _V0)

    {   /* make a shared_ptr */

    _Ref_count_obj<_Ty> *_Rx =

        new _Ref_count_obj<_Ty>( forward<_V0_t>(_V0));

    shared_ptr<_Ty> _Ret;

    _Ret._Resetp0(_Rx->_Getptr(),_Rx);

    return (_Ret);

    }

 

这就是接受0个参数和1个参数的make_shared函数的原本面貌,这之后还有4个类似的重载函数。通过这些重载函数实现了make_shared函数的功能。总体来说比较神奇

 

总结,这个函数的实现其实还算正常操作,真的只是函数重载而已,但是函数定义的这个操作是真的神秘。并且让人感慨,难道一定就要这样操作吗?只是重载函数而已,正常的去重载难道就不可以吗?只是为了重载一个函数而做这么复杂的操作,并不能体现技术实力的强大,甚至有一点炫耀技术的嫌疑。但是,隐藏在C++标准库背后的东西真的只是这么简单的吗?我一直认为一个人思考问题的方式会影响人的眼界,进而影响人能够站在的高度。如果说这一系列的操作并不是为了解决一个make_shared而设计的,而是首先有了这样的一套宏,并且库的开发者知道这一套宏可以代码展开出一系列的重载函数,那么这个地方使用这样的操作就是合情合理的。事实上,如果在c++标准库中找一找,就可以发现_VARIADIC_EXPAND_0X这个宏出现频率并不低。

C++的库中存在着太多的神奇的操作,对于C++程序员,可能不需要对其中的细节了解过深,但是如果只是为了满足好奇心,去深入了解一些这些底层的东西,就会有很多有趣的发现



猜你喜欢

转载自blog.csdn.net/fsdafsagsadgas/article/details/80380477