文档章节

std::function源码分析

htfy96
 htfy96
发布于 2016/02/09 22:33
字数 3069
阅读 2409
收藏 44
点赞 5
评论 5

##概览

###std::function std::function

template<class _Rp, class ..._ArgTypes>
class function<_Rp(_ArgTypes...)>
    : public __function::__maybe_derive_from_unary_function<_Rp(_ArgTypes...)>,
      public __function::__maybe_derive_from_binary_function<_Rp(_ArgTypes...)>
{
        __base* __f_; //points to __func
        aligned_storage< 3 *sizeof(void *)>::type 	__buf_;
        //...
};

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

__func对象可能存储的区域之一就是自带的默认缓冲区__buf_,部分MIPS指令集要求指令必须要对齐,所以这里的存储地址也要遵循平台默认的对齐方式。默认的大小是3*sizeof(void*),这是纯经验数据,对大部分的函数指针以及成员函数指针这个大小都够用(经@Anthonyhl提示,加上base*指针,__func对象总大小应该恰好是4*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

template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
class __func<_Fp, _Alloc, _Rp(_ArgTypes...)>
    : public  __base<_Rp(_ArgTypes...)>
{
    __compressed_pair<_Fp, _Alloc> __f_;
    //...
};

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

###__base

template<class _Rp, class ..._ArgTypes>
class __base<_Rp(_ArgTypes...)>
{
    __base(const __base&);
    __base& operator=(const __base&);
public:
    __base() {}
    virtual ~__base() {}
    virtual __base* __clone() const = 0;
    virtual void __clone(__base*) const = 0;
    virtual void destroy() _NOEXCEPT = 0;
    virtual void destroy_deallocate() _NOEXCEPT = 0;
    virtual _Rp operator()(_ArgTypes&& ...) = 0;
#ifndef _LIBCPP_NO_RTTI
    virtual const void* target(const type_info&) const _NOEXCEPT = 0;
    virtual const std::type_info& target_type() const _NOEXCEPT = 0;
#endif  // _LIBCPP_NO_RTTI
};

__base是一个纯虚基类,是__func类的接口,对外提供了clone(复制、移动)、destroy(析构)、operator()(调用)等函数。 ##构造 从可调用对象构造出function有以下几步:

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

###对象是否可调用

template<class _Rp, class ..._ArgTypes>
template <class _Fp>
function<_Rp(_ArgTypes...)>::function(_Fp __f,
    typename enable_if
        <
            __callable<_Fp>::value &&
            !is_same<_Fp, function>::value
        >::type*) //使用SFINAE检查该对象是否可调用,并且不是std::function(防止出现function套function的情况)。

    : __f_(0)

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

struct A
{
    void f() { cout << "called" << endl;}
};

int main()
{
    void (A::*mfp)() = &A::f;
    std::function<void(A*)> f(mfp);
    A a;
    f(&a);
}

实际上,实现__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

    template <class _Fp, bool = !is_same<_Fp, function>::value &&
                                __invokable<_Fp&, _ArgTypes...>::value> //__invokable代表是否这一些类型是否可以发生调用
        struct __callable;
    template <class _Fp>
        struct __callable<_Fp, true>
        { //如果可以发生调用,继续检查返回值是否可以转换成function的返回值
            static const bool value = is_same<void, _Rp>::value || //实际任何类型的T fun(...)都能被绑定到void fun(...),但T对void不是convertible
                is_convertible<typename __invoke_of<_Fp&, _ArgTypes...>::type,
                               _Rp>::value;
        };
    template <class _Fp>
        struct __callable<_Fp, false>
        {
            static const bool value = false;
        };

题外话,有人在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掉指向的内存地址;而如果用裸指针,构造函数抛出异常就会内存泄漏。

    if (__not_null(__f))
    {
        typedef __function::__func<_Fp, allocator<_Fp>, _Rp(_ArgTypes...)> _FF;
        if (sizeof(_FF) <= sizeof(__buf_) && is_nothrow_copy_constructible<_Fp>::value) //缓冲区够大,构造函数不抛异常
        {
            __f_ = (__base*)&__buf_; //__f_指向缓冲区
            ::new (__f_) _FF(_VSTD::move(__f)); //直接构造,间接调用了__func的移动构造函数
        }
        else
        {
            typedef allocator<_FF> _Ap;
            _Ap __a;
            typedef __allocator_destructor<_Ap> _Dp;
            unique_ptr<__base, _Dp> __hold(__a.allocate(1), _Dp(__a, 1)); //__a.allocate(1)分配了一个对象的内存,用unique_ptr保护起来
            ::new (__hold.get()) _FF(_VSTD::move(__f), allocator<_Fp>(__a)); //placement new, 在指定的内存地址调用__func的构造函数。这一步new可能会抛异常,unique_ptr在异常时会自动析构并delete内存空间
            __f_ = __hold.release(); //安全了,把指针的控制权移交给__f_
        }
    }

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

    __compressed_pair<_Fp, _Alloc> __f_; //__func的的__f_是一个compressed_pair, 不是上面的base*指针

    explicit __func(_Fp&& __f, _Alloc&& __a)
        : __f_(piecewise_construct, _VSTD::forward_as_tuple(_VSTD::move(__f)),
                                    _VSTD::forward_as_tuple(_VSTD::move(__a))) {}

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

struct Null {};
struct Test { int a; };

struct B
{
    Null n;
    Test c;
};

    cout << sizeof(Null) << " "<< sizeof(Test)<<" "<<sizeof(B)<<endl; //1 4 8

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

struct B1 : private Null
{
};
struct B2 : private B1, private Test
{
};
    cout << sizeof(B1)<<" "<<sizeof(B2) << endl; // 1 4

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对象。但是,构造函数不具有多态性,怎么根据父类的指针来获得子类的拷贝呢?这是一种常用的技巧:

virtual SuperClass* SubClass::clone() { return new SubClass(*this); } //相当于多态new
virtual SuperClass* SubClass::clone(SuperClass* p) { return new (p) SubClass(*this); } //多态placement new

###复制构造

//.__f_是指向__func对象的指针
template<class _Rp, class ..._ArgTypes>
function<_Rp(_ArgTypes...)>::function(const function& __f)
{
    if (__f.__f_ == 0) //未初始化
        __f_ = 0;
    else if (__f.__f_ == (const __base*)&__f.__buf_) //另一个对象的__func存放在自身的缓冲区内,既然在缓冲区内能放下,也应该能在我的缓冲区内放下
    {
        __f_ = (__base*)&__buf_; //自己指向自身的缓冲区
        __f.__f_->__clone(__f_); //相当于new (__f_) __func(另一个__func),把另一个__func复制到自身缓冲区内
    }
    else
        __f_ = __f.__f_->__clone(); //放不下了,让它新开一块内存复制到其中,然后自己指过去
}

###移动构造

template<class _Rp, class ..._ArgTypes>
function<_Rp(_ArgTypes...)>::function(function&& __f) _NOEXCEPT
{
    if (__f.__f_ == 0)
        __f_ = 0;
    else if (__f.__f_ == (__base*)&__f.__buf_) //__func在缓冲区,缓冲区够用
    {
        __f_ = (__base*)&__buf_; //不能直接指到对方缓冲区去,因为对方__buf会随对象析构销毁掉
        __f.__f_->__clone(__f_); //还是要复制到自己的缓冲区来
    }
    else
    {
        __f_ = __f.__f_; //对方的__func在堆上,直接指过去
        __f.__f_ = 0; //把对方的__f_指空
    }
}

##调用

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

template<class _Rp, class ..._ArgTypes>
_Rp
function<_Rp(_ArgTypes...)>::operator()(_ArgTypes... __arg) const
{
#ifndef _LIBCPP_NO_EXCEPTIONS
    if (__f_ == 0)
        throw bad_function_call();
#endif  // _LIBCPP_NO_EXCEPTIONS
    return (*__f_)(_VSTD::forward<_ArgTypes>(__arg)...); //调用内部__func对象的operator()
}
ArgTypeforward<ArgType>
Tstatic_cast<T&&>
T&static_cast<T&>
T&&static_cast<T&&>

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

template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
_Rp
__func<_Fp, _Alloc, _Rp(_ArgTypes...)>::operator()(_ArgTypes&& ... __arg) //完美转发
{
    typedef __invoke_void_return_wrapper<_Rp> _Invoker; //后述,与invoke的特殊语法有关
    return _Invoker::__call(__f_.first(), _VSTD::forward<_ArgTypes>(__arg)...); //__f_.first()即可调用对象
}

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

int foo() { return 42; }
void bar() { return foo(); } //报错,int不能转成void
void bar2() { foo(); } //针对void返回值这样才对
function<void()> f(foo); //合法

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

template <class _Ret>
struct __invoke_void_return_wrapper
{
    template <class ..._Args>
    static _Ret __call(_Args&&... __args)
    {
        return __invoke(_VSTD::forward<_Args>(__args)...);
    }
};

template <>
struct __invoke_void_return_wrapper<void>
{
    template <class ..._Args>
    static void __call(_Args&&... __args)
    {
        __invoke(_VSTD::forward<_Args>(__args)...);
    }
};

仔细思考一下整个调用过程,发现还是具有负担的: 对于形参是T的对象来说,

void foo(A) {}
A a;

foo(a); //a被复制构造一次

function<void(A)> f(foo);
f(a); //先被复制构造一次,再被移动构造一次
// 等价于
A b(a); //这个复制发生在function::operator()的形参表里
foo(forward<A>(b)); //发生了移动构造

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

##总结

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

© 著作权归作者所有

共有 人打赏支持
htfy96
粉丝 8
博文 5
码字总数 10027
作品 0
闵行
程序员
加载中

评论(5)

htfy96
htfy96

引用来自“Anthonyhl”的评论

不错,但对于最后的性能比较,可能点误解。
foo(forward<A>(b)); 这个过程,并不会移动构造。
这一点forward和move一样,只是保证类型正确,不会增加运行时开销。

另外,sizeof(void*)*3,除了大部分平台都可以放下函数指针外,加上base*那个指针,正好是4个指针长度。
1. forward(b) == b的右值引用,移动构造发生在 利用这个右值引用 初始化foo的形参过程中 2. 是4个指针长度这一点的确没想到,学习了。
黄亮Anthony
黄亮Anthony
不错,但对于最后的性能比较,可能点误解。
foo(forward<A>(b)); 这个过程,并不会移动构造。
这一点forward和move一样,只是保证类型正确,不会增加运行时开销。

另外,sizeof(void*)*3,除了大部分平台都可以放下函数指针外,加上base*那个指针,正好是4个指针长度。
htfy96
htfy96

引用来自“cgcgbcbc”的评论

c++的库实现看起来永远是天书一样10
linc++标准库还比较好读,libstdcxx才难读 主要因为里面的很多技巧如果整天写业务逻辑根本不会用到,但不能说它们就不重要
SHIHUAMarryMe
SHIHUAMarryMe
这么换行我好晕
cgcgbcbc
cgcgbcbc
c++的库实现看起来永远是天书一样10
实现一个简单的编译器

简单的说 就是语言翻译器,它一般将高级语言翻译成更低级的语言,如 GCC 可将 C/C++ 语言翻译成可执行机器语言,Java 编译器可以将 Java 源代码翻译成 Java 虚拟机可以执行的字节码。 编译器...

Yunba
2016/11/07
30
0
ReactNative源码篇:通信机制

关于作者 郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢迎交流技术方面的问题,可以去我的Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。 更多文章:git...

郭孝星
2017/09/28
0
0
MATLAB感悟(4)--主成分分析

目的描述 出于模型的需要,我们的团队选择做一次主成分分析,通常这部分在队伍中是会有同学专门负责这块的,至于为什么笔者就不在这里多说了。 解决思路 在MATLAB中封装了有关因子分析的方法...

T-newcomer
03/01
0
0
ReactNative源码篇:启动流程

关于作者 郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢迎交流技术方面的问题,可以去我的Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。 更多文章:git...

郭孝星
2017/09/28
0
0
Redhat-9.0中安装开源软件CVSSearch编译出错

我在redhat中安装开源软件CVSSearch,把需要提前安装的sleepycat(berkelly db)和omsee(xapian)都装好了,zlib也装了,最后安装CVSSearch的时候,configure的时候发现有一项是no的,就是c++...

王沿途
2012/12/20
89
2
c++ 11 多线程处理(1)

几种线程的方式 创建线程 通过函数指针来创建一个线程 使用函数对象创建一个线程 使用 匿名函数来创建线程 不同线程之间的区别 第一个线程都会有一个ID成员函数指定关联线程对象的IDstd::th...

罗布V
2016/07/07
53
0
C++中的匿名函数(译)

C++11最令人兴奋的特性之一就是能够创建匿名函数(lambda functions),有时也被称为闭包(closures)。这意味着什么?lambda function是一个可以内联写在代码里的函数(通常被传递给另一个函...

LsDimplex
2016/11/03
35
0
boost源码剖析之:多重回调机制signal(上)

boost源码剖析之:多重回调机制signal(上) 刘未鹏 C++的罗浮宫(http://blog.csdn.net/pongba) boost库固然是技术的宝库,却更是思想的宝库。大多数程序员都知道如何应用command,observer等模...

长平狐
2012/08/28
799
0
[C/C++]完整揭秘VS2010关于function和bind的实现

很久之前我就对C++里面的function非常感兴趣,也探究出了一些成果。 [C/C++]std::tr1::function源码剖析(一) [C/C++]std::tr1::function源码剖析(二) 这两篇文章是对VS2010中如何实现fun...

梁欢
2013/10/25
0
0
重读经典-《Effective C++》Item4:确定对象被使用前已先被初始化

本博客(http://blog.csdn.net/livelylittlefish )贴出作者(三二一@小鱼)相关研究、学习内容所做的笔记,欢迎广大朋友指正! 1. 永远在使用对象之前先将它初始化 (1) 对于无任何成员的内置...

晨曦之光
2012/03/09
175
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

实现异步有哪些方法

有哪些方法可以实现异步呢? 方式一:java 线程池 示例: @Test public final void test_ThreadPool() throws InterruptedException { ScheduledThreadPoolExecutor scheduledThre......

黄威
今天
0
0
linux服务器修改mtu值优化cpu

一、jumbo frames 相关 1、什么是jumbo frames Jumbo frames 是指比标准Ethernet Frames长的frame,即比1518/1522 bit大的frames,Jumbo frame的大小是每个设备厂商规定的,不属于IEEE标准;...

六库科技
今天
0
0
牛客网刷题

1. 二维数组中的查找(难度:易) 题目描述 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入...

大不了敲一辈子代码
今天
0
0
linux系统的任务计划、服务管理

linux任务计划cron 在linux下,有时候要在我们不在的时候执行一项命令,或启动一个脚本,可以使用任务计划cron功能。 任务计划要用crontab命令完成 选项: -u 指定某个用户,不加-u表示当前用...

黄昏残影
昨天
0
0
设计模式:单例模式

单例模式的定义是确保某个类在任何情况下都只有一个实例,并且需要提供一个全局的访问点供调用者访问该实例的一种模式。 实现以上模式基于以下必须遵守的两点: 1.构造方法私有化 2.提供一个...

人觉非常君
昨天
0
0
《Linux Perf Master》Edition 0.4 发布

在线阅读:https://riboseyim.gitbook.io/perf 在线阅读:https://www.gitbook.com/book/riboseyim/linux-perf-master/details 百度网盘【pdf、mobi、ePub】:https://pan.baidu.com/s/1C20T......

RiboseYim
昨天
1
0
conda 换源

https://mirrors.tuna.tsinghua.edu.cn/help/anaconda/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/conda config --add channels https://mir......

阿豪boy
昨天
1
0
Confluence 6 安装补丁类文件

Atlassian 支持或者 Atlassian 缺陷修复小组可能针对有一些关键问题会提供补丁来解决这些问题,但是这些问题还没有放到下一个更新版本中。这些问题将会使用 Class 类文件同时在官方 Jira bug...

honeymose
昨天
0
0
非常实用的IDEA插件之总结

1、Alibaba Java Coding Guidelines 经过247天的持续研发,阿里巴巴于10月14日在杭州云栖大会上,正式发布众所期待的《阿里巴巴Java开发规约》扫描插件!该插件由阿里巴巴P3C项目组研发。P3C...

Gibbons
昨天
1
0
Tomcat介绍,安装jdk,安装tomcat,配置Tomcat监听80端口

Tomcat介绍 Tomcat是Apache软件基金会(Apache Software Foundation)的Jakarta项目中的一个核心项目,由Apache、Sun和其他一些公司及个人共同开发而成。 java程序写的网站用tomcat+jdk来运行...

TaoXu
昨天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部