STL 源码简析 – std::function (下)

上一篇文章介绍了 libc++ 中 std::function 的实现。它使用了虚表来在一个类型擦除后的容器中精确地调用某些具体类型的对象成员函数。

但是我们要知道,虚表虽然能在类型不明朗的情况下给我们带来动态分派的能力,它的开销也是极大的。虚表的实现可以看我另一篇文章的介绍,概括一下大概有,对于每个 function 模板的特化,同时对应着每个基类 __base 的特化,都会生成一张基类虚表(尽管是纯虚但是并不会被优化);同时,对于每个不同的可调用类和 __func ,它也会生成一个带基类信息的虚表——这就会导致二进制文件体积狂增,特别是在开启 RTTI 的情况下。除了空间上的开销之外,时间上也因为多了对虚表的处理,需要多级指针多次解引用计算偏移,当调用次数增加的时候,消耗也会相应增加。

虚表本质上是什么?是函数指针和一些 RTTI 而已,我们实际上并不需要 RTTI ,我们需要的函数又只有确定的那么几个,那为什么不自己手动维护函数指针呢?

这就是 libstdc++ 的实现了, libstdc++ 为了减少实例的体积,在这之上做了少许优化。

地狱 —— libstdc++ 实现

libstdc++ 的 function 源码在 bits/std_function.h 中。它的代码很神奇,除了一个异常派生类 bad_function_call 的析构之外,你完全找不到任何的虚函数。就 function 的实现也是不使用任何虚的东西的。

function 类的声明在 126 行,定义在 373-611 行。相对于 libc++ 的行数,算是比较长的,尽管大部分都是注释或空行。

定义在 function 中,其自身的成员变量只有 609-610 的一个函数指针 _M_invoker ,它将会在实例非空的时候指向将要调用的可调用对象 operator() 本身。 function 还有一些其它的成员则是从它的一个基类 _Function_base 提供的,我们先将目光转向这个 base 。

基类 _Function_base 中,除了构造、析构,还有两个成员变量之外,其它都是内部嵌套模板类 _Base_manager 的静态成员函数, _Base_manager 还有一个派生类 _Function_handler_Function_base 的两个成员变量的类型签名中均涉及到了一个叫做 _Any_data 的联合体 union 。

libstdc++ 的 function 实现,主要牵扯到的数据结构就是这几个了,只要把这几个结构的作用理清, function 的实现就很清晰地摆在眼前了。

_Any_data: 只是个 aligned_storage

  class _Undefined_class;

  union _Nocopy_types
  {
    void*       _M_object;
    const void* _M_const_object;
    void (*_M_function_pointer)();
    void (_Undefined_class::*_M_member_pointer)();
  };

  union [[gnu::may_alias]] _Any_data
  {
    void*       _M_access()       { return &_M_pod_data[0]; }
    const void* _M_access() const { return &_M_pod_data[0]; }

    template<typename _Tp>
      _Tp&
      _M_access()
      { return *static_cast<_Tp*>(_M_access()); }

    template<typename _Tp>
      const _Tp&
      _M_access() const
      { return *static_cast<const _Tp*>(_M_access()); }

    _Nocopy_types _M_unused;
    char _M_pod_data[sizeof(_Nocopy_types)];
  };

_Undefined_class 是一个故意未定义的帮助类,我们可以无视它。 _Nocopy_types 则是各种指针类型, C/C++ 中不同的指针也可能有不同的大小,它的作用只是确定了指针最大大小而已,它决定了 _Any_data 只有一个指针大小,没有具体的类型系统上的作用。

_Any_data 的成员函数主要用来做类型转换,成员也只有一个 _Nocopy_types 和一样大小的 char 数组。它的作用和 libc++ 实现中的 std::aligned_storage 完全相同。

may alias: 和 strict-aliasing 的那点孽缘

_Any_data 的定义前有一个 [[gnu::may_alias]] ,这种形式是 C++11 引入的用于统一编译器 attribute 的通用语法,在 g++ 中等价于 __attribute__((may_alias)) 。 这是一个明显依赖于编译器实现的特性,所以 gnu:: 这部分就是用来让编译器决定自己要不要去理解这个 attribute 的。

关于这个 may_alias 属性,程序员可以用它来阻止编译器的 strict aliasing 假设。 strict aliasing 是 C/C++ 标准规定的,程序员必须遵守的约定,它阻止了指针在不兼容的类型之间互相解引用——如使用 int * 读取一个 std::string 实例这种行为。因为有这条规定,所以编译器可以假设代码中所有的指针读写都有正确的指向,这可用于优化内存读写次数。

不过虽然是规定,它只是个约定,程序员不遵守的时候并不强制编译器报错,因此程序员仍然可以写出指针乱指的程序。特别是性能需求高的程序中,可能会需要显式地直接在两个类型中转换以加速程序(传统的避开 UB 的解决方案是:复制一份数据到需要的类型指针指向的内存),这时候就需要推翻编译器的 strict-aliasing 假设,可以通过开启编译选项 -fno-strict-aliasing 或者采用 __attribute__((may_aliasing)) 属性定义数据类型来关闭编译器的优化。

_Base_manager: 可调用对象主管

回到 _Function_base 中, _Function_base 的定义共 137 行,其中 113 行都是 _Base_manager_Base_manager 是一个模板类,其模板参数即为可调用类。该类中并没有任何成员变量和非静态成员函数,它同样也不会创建任何实例。事实上,它的作用并不是产生实例,而是为不同的可调用类型分别提供统一的接口函数。这点上来说它和 libc++ 中的 __func 功能是一致的。

	static const bool __stored_locally =
	(__is_location_invariant<_Functor>::value
	 && sizeof(_Functor) <= _M_max_size
	 && __alignof__(_Functor) <= _M_max_align
	 && (_M_max_align % __alignof__(_Functor) == 0));

	typedef integral_constant<bool, __stored_locally> _Local_storage;

这几行定义的静态成员作用是判断可调用对象能否储存于 _Any_data 中, _M_max_size_M_max_align 分别都是 _Nocopy_types 的大小和对齐。在简单判断的时候只需要用 __stored_locally 变量即可, _Local_storage 类型是用于在静态分派时作为分支条件的。

接下来的几个函数都比较朴素,只是对于特定可调用类的拷贝、构造、析构的一个封装,这里不用特地介绍。稍微关注一下 _M_manager 这个函数,这是一个比较重要的任务分派函数。

	static bool
	_M_manager(_Any_data& __dest, const _Any_data& __source,
		   _Manager_operation __op)
	{
	  switch (__op)
	    {
#if __cpp_rtti
	    case __get_type_info:
	      __dest._M_access<const type_info*>() = &typeid(_Functor);
	      break;
#endif
	    case __get_functor_ptr:
	      __dest._M_access<_Functor*>() = _M_get_pointer(__source);
	      break;

	    case __clone_functor:
	      _M_clone(__dest, __source, _Local_storage());
	      break;

	    case __destroy_functor:
	      _M_destroy(__dest, _Local_storage());
	      break;
	    }
	  return false;
	}

函数用到了一个 _Manager_operation 类型,这实际上只是一个枚举类型而已。作用当然只是让读代码的人能一眼看懂每种参数能干什么的:

  enum _Manager_operation
  {
    __get_type_info,
    __get_functor_ptr,
    __clone_functor,
    __destroy_functor
  };

manager 函数就是利用这几个枚举值,来分派到具体的子功能上。而 function 对象本身,则保存它对应的 manager 函数指针,再在需要拷贝或者删除的时候通过保存的函数指针来调用到类型对应的 manager 函数中,具体的流程在下文中分析。

剩下的两个成员函数则是用于初始化可调用对象的函数。初始化只需要在一开始执行一次就行,因此不需要 manager 来维护它的位置。

	_M_init_functor(_Any_data& __functor, _Functor&& __f, true_type)
	{ ::new (__functor._M_access()) _Functor(std::move(__f)); }

	static void
	_M_init_functor(_Any_data& __functor, _Functor&& __f, false_type)
	{ __functor._M_access<_Functor*>() = new _Functor(std::move(__f)); }

显然,这个 true_typefalse_type 用了静态分派的手法。在调用的时候就是通过传入一个 _Local_storage 的实例来匹配到具体的重载的。

接下来就回到 _Function_base 了,就是几个构造函数和成员变量的定义。显然 _M_functor 保存的就是可调用函数本身(或者是指向它的指针), _M_manager 就是上文所说的 manager 函数的指针。

_Function_handler: operator() 呼出

  template<typename _Signature, typename _Functor>
    class _Function_handler;

  template<typename _Res, typename _Functor, typename... _ArgTypes>
    class _Function_handler<_Res(_ArgTypes...), _Functor>
    : public _Function_base::_Base_manager<_Functor>
    {
      typedef _Function_base::_Base_manager<_Functor> _Base;

    public:
      static _Res
      _M_invoke(const _Any_data& __functor, _ArgTypes&&... __args)
      {
	return (*_Base::_M_get_pointer(__functor))(
	    std::forward<_ArgTypes>(__args)...);
      }
    };

  template<typename _Functor, typename... _ArgTypes>
    class _Function_handler<void(_ArgTypes...), _Functor>
    : public _Function_base::_Base_manager<_Functor>
    {
      typedef _Function_base::_Base_manager<_Functor> _Base;

     public:
      static void
      _M_invoke(const _Any_data& __functor, _ArgTypes&&... __args)
      {
	(*_Base::_M_get_pointer(__functor))(
	    std::forward<_ArgTypes>(__args)...);
      }
    };

_Function_handler 的特化挺长的,上面只摘抄了一段(剩下在下面)。当然,这里也是一个右值引用传递。 _Function_handler 在这里的作用和 libc++ 中的 __func 类类似,只是它只负责一个 operator() 调用时参数的传递,其它函数均在它的基类 _Function_base::_Base_manager 中。

实现上没什么亮点,应该说就是该怎样就怎样吧,基本都和 libc++ 的一样。

  template<typename _Class, typename _Member, typename... _ArgTypes>
    class _Function_handler<void(_ArgTypes...), _Member _Class::*>
    : public _Function_base::_Base_manager<
		 _Simple_type_wrapper< _Member _Class::* > >
    {
      typedef _Member _Class::* _Functor;
      typedef _Simple_type_wrapper<_Functor> _Wrapper;
      typedef _Function_base::_Base_manager<_Wrapper> _Base;

    public:
      static bool
      _M_manager(_Any_data& __dest, const _Any_data& __source,
		 _Manager_operation __op)
      {
	switch (__op)
	  {
#if __cpp_rtti
	  case __get_type_info:
	    __dest._M_access<const type_info*>() = &typeid(_Functor);
	    break;
#endif
	  case __get_functor_ptr:
	    __dest._M_access<_Functor*>() =
	      &_Base::_M_get_pointer(__source)->__value;
	    break;

	  default:
	    _Base::_M_manager(__dest, __source, __op);
	  }
	return false;
      }

      static void
      _M_invoke(const _Any_data& __functor, _ArgTypes&&... __args)
      {
	std::__invoke(_Base::_M_get_pointer(__functor)->__value,
		      std::forward<_ArgTypes>(__args)...);
      }
    };

第三段特化,正如模板参数所展现出来的那样,是针对成员函数指针的特化。我们知道 function 可以从类成员函数指针构造,接受成员函数作为可调用类型。

这个 _Simple_type_wrapper 我研究了一会儿,并不知道它的存在意义何在……希望有知情人士指出其作用。我甚至不知道它为什么要特地加上成员函数的特化,它完全可以做到全部使用 std::__invoke 调用统一处理的……如果说是为了少一次到 std::__invoke 中的移动构造,那为什么成员函数的特化不直接调用呢?

  template<typename _Class, typename _Member, typename _Res,
	   typename... _ArgTypes>
    class _Function_handler<_Res(_ArgTypes...), _Member _Class::*>
    : public _Function_handler<void(_ArgTypes...), _Member _Class::*>
    {
      typedef _Function_handler<void(_ArgTypes...), _Member _Class::*>
	_Base;

     public:
      static _Res
      _M_invoke(const _Any_data& __functor, _ArgTypes&&... __args)
      {
	return std::__invoke(_Base::_M_get_pointer(__functor)->__value,
			     std::forward<_ArgTypes>(__args)...);
      }
    };

这是针对返回类型非 void 的类成员函数的特化。继承了 void 用于代码复用,只有 _M_invoke 重写了,只是加了一个 return 而已,也不展开说了。

function 本体

  template<typename _From, typename _To>
    using __check_func_return_type
      = __or_<is_void<_To>, is_same<_From, _To>, is_convertible<_From, _To>>;
      template<typename _Func,
	       typename _Res2 = typename result_of<_Func&(_ArgTypes...)>::type>
	struct _Callable : __check_func_return_type<_Res2, _Res> { };

      // Used so the return type convertibility checks aren't done when
      // performing overload resolution for copy construction/assignment.
      template<typename _Tp>
	struct _Callable<function, _Tp> : false_type { };

      template<typename _Cond, typename _Tp>
	using _Requires = typename enable_if<_Cond::value, _Tp>::type;

这是开头的几行,显然也是用来做 SFINAE 的。具体检查了返回类型是否为 void 或可转换为 function 定义类型。 _Requires 则是最后用于模板类型中的,它实际上就是一个 enable_if_t 的别名,至于 enable_if ,只有在它第一个参数为 true 的时候才会在结构体内定义 type 为第二个模板参数,留空默认为 void 。这一般用于函数声明时作为返回类型,用于 SFINAE 。

接下来一堆代码都没有什么太大的意义,基本都是很长的注释加上一两行代码,这里先略过。

_M_manager 的第一次调用

      function&
      operator=(nullptr_t) noexcept
      {
	if (_M_manager)
	  {
	    _M_manager(_M_functor, _M_functor, __destroy_functor);
	    _M_manager = nullptr;
	    _M_invoker = nullptr;
	  }
	return *this;
      }

这是一个将对象赋为空值的函数(废话),在这里我们第一次见到了如何使用 _M_manager 来析构一个 function 对象。

流程是当对象本身非空的时候,通过 _M_manager 指针调用 _Base_manager::_M_destroy ,通过 _Local_storage 的类型为 true_typefalse_type 判断对象是储存在 _Any_data _M_functor 中或是 _M_functor 只是个指向对象的指针,并相应地静态分派到对应函数中,原地析构或 delete 指针;最后的最后,将 _Function_base 中的 _M_functor_M_manager 置空用于 _M_empty 判断和防止 UAF 。

Swap

      void swap(function& __x) noexcept
      {
	std::swap(_M_functor, __x._M_functor);
	std::swap(_M_manager, __x._M_manager);
	std::swap(_M_invoker, __x._M_invoker);
      }

libstdc++ 中的 swap 出奇地简单。相比于 libc++ 中的 swap 成员函数,首先少了自赋值保护。然后 libstdc++ 根本没有考虑对象的位置,它直接 std::swap 了三个指针。

这当然没问题,事实上,只要自身保存着所有的状态,不依赖全局的状态,就没有什么是不能直接 swap 的。

最后的构造

  template<typename _Res, typename... _ArgTypes>
    function<_Res(_ArgTypes...)>::
    function(const function& __x)
    : _Function_base()
    {
      if (static_cast<bool>(__x))
	{
	  __x._M_manager(_M_functor, __x._M_functor, __clone_functor);
	  _M_invoker = __x._M_invoker;
	  _M_manager = __x._M_manager;
	}
    }

  template<typename _Res, typename... _ArgTypes>
    template<typename _Functor, typename, typename>
      function<_Res(_ArgTypes...)>::
      function(_Functor __f)
      : _Function_base()
      {
	typedef _Function_handler<_Res(_ArgTypes...), _Functor> _My_handler;

	if (_My_handler::_M_not_empty_function(__f))
	  {
	    _My_handler::_M_init_functor(_M_functor, std::move(__f));
	    _M_invoker = &_My_handler::_M_invoke;
	    _M_manager = &_My_handler::_M_manager;
	  }
      }

这是 function 两个略有不同的构造函数。前者是拷贝构造,同样调用了 _M_manager ,套路和上文析构的类似,不展开。后者是从一个可调用对象构造,好,我们先来看它的声明:

      template<typename _Functor,
	       typename = _Requires<__not_<is_same<_Functor, function>>, void>,
	       typename = _Requires<_Callable<_Functor>, void>>
	function(_Functor);

上文提到过 _Requires 即为 enable_if_t ,这里用于模板参数中 SFINAE ,只有当 _Functor 满足两个条件时构造才会被定义,才能构造 function

回到函数定义本体,值得关注的是 _My_handle 这个类型,它就是上文解释了的 _Function_handler ,我们在定义中取了它(继承自 _Function_base::_Base_manager )的静态成员函数指针并保存到 function 对象中。

于是,虽然 function 本身并不知道这个指针具体是什么类型,但是它知道传入什么参数可以调用它。而这个函数指针知道自己是什么类型,这就可以通过函数指针来找到所储存的类的每个成员函数。就这么轻易地在对象内部实现了一个极其简易的虚表。

推导指引

#if __cpp_deduction_guides >= 201606
  template<typename>
    struct __function_guide_helper
    { };

  template<typename _Res, typename _Tp, bool _Nx, typename... _Args>
    struct __function_guide_helper<
      _Res (_Tp::*) (_Args...) noexcept(_Nx)
    >
    { using type = _Res(_Args...); };

  template<typename _Res, typename _Tp, bool _Nx, typename... _Args>
    struct __function_guide_helper<
      _Res (_Tp::*) (_Args...) & noexcept(_Nx)
    >
    { using type = _Res(_Args...); };

  template<typename _Res, typename _Tp, bool _Nx, typename... _Args>
    struct __function_guide_helper<
      _Res (_Tp::*) (_Args...) const noexcept(_Nx)
    >
    { using type = _Res(_Args...); };

  template<typename _Res, typename _Tp, bool _Nx, typename... _Args>
    struct __function_guide_helper<
      _Res (_Tp::*) (_Args...) const & noexcept(_Nx)
    >
    { using type = _Res(_Args...); };

  template<typename _Res, typename... _ArgTypes>
    function(_Res(*)(_ArgTypes...)) -> function<_Res(_ArgTypes...)>;

  template<typename _Functor, typename _Signature = typename
	   __function_guide_helper<decltype(&_Functor::operator())>::type>
    function(_Functor) -> function<_Signature>;
#endif

我将推导指引放到了最后来讲,因为这是 C++17 的新东西(实际上今天 C++17 也不新了, C++20 都基本定型了)。

推导指引是什么呢?简单举个例子,在我们有推导指引之前,如果想要直接构造一个 pair (不使用 make_pair ),我们只能把它的两个模板参数都写明。如 pair<string, int>{"first"s, 1} ,尽管它的两个参数类型已经明确了,我们还是无法在构造的时候略去模板参数。所以才有了 make_pair ,函数它本身就可以自动推导模板参数。

而有了类型推导指引之后,程序员就可以在类型明确的情况下省去再写一遍模板参数的麻烦了。如 pair{"first"s, 1} ,编译器就可以通过头文件中声明的推导指引来推导出这个对象的实际类型是 pair<string, int>

function 也类似,原先我们并没有 make_function 这种函数,所以所有的参数都必须手动写明。这种情况在参数类型改变、参数改变的时候都会对程序员造成一定的麻烦。当一切交给编译器之后,编译器自然可以(通过头文件的推导指引)推导出正确的类型。即使参数的类型改变,程序员也无需改动任何代码。

首先看 642-643 行,这是针对入参为函数指针的重载,含义是推导出来的 function 模板参数和函数指针的函数类型完全一致。注意左侧是构造函数的形式,右侧是构造的 function 类型。这很好理解。

然后看 647 行, decltype(&_Functor::operator()) 是取 _Functor 中的 operator() 函数的原型。这是一个取可调用对象的 operator() 类型的技巧,对于闭包类型也通用。唯一的问题是当类有两个 operator() 重载时就不管用了,此时必须由程序员指定具体用哪个。

最后几个 __function_guide_helper 只是针对 const 和引用类型的情况重载了一下,无须多言。

发表评论