std::function源码分析


本次分析的是libcpp(_LIBCPP_VERSION=3700)的std::function这个类。它作为可调用对象的适配器,在C++11及之后的标准库中发挥了巨大的作用。尤其是引入的lambda表达式,如果不通过std::function就难以保存在容器中。它的实现利用到了C++的很多特性,在此进行分析。

概览

std::function

std::function最重要的部分就是这个__base*指针,及其所指向的存储了实际可调用对象的多态类__func__base类充当了__func类的接口,定义了cloneoperator()等纯虚函数。

__func对象可能存储的区域之一就是自带的默认缓冲区__buf_,部分MIPS指令集要求指令必须要对齐,所以这里的存储地址也要遵循平台默认的对齐方式。默认的大小是3*sizeof(void*),这是纯经验数据,对大部分的函数指针以及成员函数指针这个大小都够用。但因为可调用对象大小千变万化,所以实际存储的区域可能也会在新开的堆上

std::function类继承自__maybe_derive_from_unary_function__maybe_derive_from_binary_function两个类。这两个类在函数分别满足ResultT f(ArgT)ResultT f(Arg1T, Arg2T)形式的时候,分别会特化继承std::unary_function<ResultT, ArgT>std::binary_function<ResultT, arg1T, arg2T>

这两个类是C++11之前对两种特殊可调用对象的静态接口,其内只有typedef,在C++11之后已经deprecated,C++17后将移除,这里继承这两个接口只是为了兼容目的。关于C++11之前的<functional>分析,详见这篇文章

__func

__func是实际存储可调用对象的类,其继承了__base这个接口。可调用对象与allocator都被存储在一个__compressed_pair当中。

__base

__base是一个纯虚基类,是__func类的接口,对外提供了clone(复制、移动)、destroy(析构)、operator()(调用)等函数。

构造

从可调用对象构造出function有以下几步:

  • 检查该对象是否可调用
  • 若缓冲区__buf_不够存放可调用对象,新开内存
  • __f_指向的内存区域调用placement new,移动构造可调用对象。

对象是否可调用

在滚到下面之前,先猜一下__callable是怎么实现的。注意以下代码也是合法的,还要考虑reference_wrapper、返回值转化等各种形式:

实际上,实现__callable主要依赖于invoke的实现,invoke规定了一个统一的调用方式,将于C++17标准中出现。不论是f(a,b)还是(f.*a)(b)f是可调用对象,a是成员函数指针)还是(a->*f)(b)a是可调用对象指针,f是成员函数指针),都可以以invoke(f,a,b)的形式调用。

知道了这个函数,我们只要规定invoke可以调用,并且返回值可以转换成std::function规定的返回类型的函数就是callable

题外话,有人在C++17当中提出统一x.f(a,b)f(x,a,b),应该会给invoke当前的复杂情况带来一点帮助:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4165.pdf

内存分配与构造

function

为了保证异常安全。分为两种情况:若自带的__buf_大小够大,且可调用对象的构造函数不抛出异常,则直接构造;否则,则用unique_ptr来处理allocator分配出的内存地址,再在上面调用构造函数,这样即使构造函数抛出了异常,unique_ptr也会自动delete掉指向的内存地址;而如果用裸指针,构造函数抛出异常就会内存泄漏。

__func

这个构造函数之中调用了__func类的构造函数:

首先介绍下这个compressed_pair, 众所周知C++的空类默认也会占空间:

但这样在有内存对其的时候其实浪费了大量的存储空间,特别是对于function这类小对象来说节约空间非常重要。对于空类Null,一个继承自它的类B2,且B2非空类,则B2不会因为Null类的继承而像上例中的内含一样占用空间:

compressed_pair就用了这种技巧来压缩内存,这种技术在boost::compressed_pair当中已经有成熟的库,这里libc++内部也制作了一个自己的__compressed_pair

再来说说这个piecewise_construct。一般使用pair时,我们都是利用make_pair(T1(arg1, arg2), T2(arg))这样来构造。实际上,发生了以下的步骤:

  • 构造出一个T1的xvalue(消亡值,属于右值),匹配上make_pair(T1&&, T2&&)
  • make_pair把这两个右值引用传递给pair<T1, T2>(T1&& t1, T2&& t2)
  • pair的构造函数把内部的first, second对象在初始化列表中以first(t1), second(t2)形式初始化,这个t1,t2都是右值,所以调用了移动构造函数

相当于我们构造了一个临时对象,然后又调用了移动构造函数。这样就有一个问题:如果没有移动构造函数怎么办?piecewise_construct就是为此而生的。使用pair<T1, T2>(piecewise_construct, tuple<Args...>&& t1, tuple<Args...>&& t2)这样的形式,最终初始化列表中会直接转化成: first(std::forward<_Args1>(std::get<_I1>( __first_args))...),即这些参数会被直接传递给first,second对象,直接在pair的构造函数内初始化first second,而不是先在形成参数时构造出临时对象,再移动过去。这样既有比较好的性能,也不需要具有first,second具有复制、移动构造函数。

复制与移动

复制与移动实际上都是操作内部的__func对象。但是,构造函数不具有多态性,怎么根据父类的指针来获得子类的拷贝呢?这是一种常用的技巧:

复制构造

移动构造

调用

调用的时候先检查内部的__f_指针是否为空,若空则抛异常,否则调用__f_指向的__func对象的operator():

<th style="text-align: center;">
  <code>forward&lt;ArgType&gt;</code>
</th>
<td style="text-align: center;">
  <code>static_cast&lt;T&&&gt;</code>
</td>
<td style="text-align: center;">
  <code>static_cast&lt;T&&gt;</code>
</td>
<td style="text-align: center;">
  <code>static_cast&lt;T&&&gt;</code>
</td>

std::forward作用如其名,即将参数向前传递。原先的ArgType=T时,在调用这个函数时已经复制过了一遍,因此复制过的值可以作为右值,forward<T>(t)t转成了右值。而对于原先是左值、右值引用的来说,则不能都作为右值处理,而应保持它们本身的类别。

这里不直接return invoke(__f_.first(), ...)的原因是,如果__f_的返回值是void,但实际可调用对象返回值,就会出错:

所以针对void返回值要特化一下:

仔细思考一下整个调用过程,发现还是具有负担的:

对于形参是T的对象来说,

所以在C++11中,移动构造非常重要,如果能够定义移动构造函数请务必定义。否则该例就会退化到两次复制构造,如果在传递大对象时将是不小的负担。

总结

  • std::function是自带的可调用对象适配器。它通过内部__f_指针调用所指向的__func类对象的虚方法来实现多态的函数调用、newplacement new。其中内带了一个大小是3*sizeof(void*)的缓冲区,小对象将被分配在缓冲区上,大对象将另外在堆上分配内存存储。
  • __func对象利用了compressed_pair技术来压缩存储的可调用对象 - Allocator对,并利用piecewise_construct来就地构造这两个对象,能够处理这两个类没有移动复制构造函数的情况,也提高了性能。
  • std::function在形参是非引用时会多发生一次移动构造,可能成为性能的瓶颈。