浅谈std::bind的实现

bind这个东西争议很多,用起来很迷,而且不利于编译优化,很多人都推荐用lambda而非bind。简单说,bind就是通过库抽象实现了lambda里需要写进语言标准的东西,变量捕获,参数绑定,延迟求值等。但是以此带来的缺陷就是,虽然bind生成的可调用对象的结构是编译期确定的,但是它的值,尤其是被调用的函数,全部是在运行期指定的,并且可调用对象也只是一个普通的类,因此很难进行优化。除此之外,标准库的bind实现,只提供了20个placeholder进行参数绑定,无法扩展,这也是实现的一个坑。因此,在有条件的情况下,应该使用lambda而非bind,lambda是写入语言标准的特性,编译器面对一个你写的lambda,和bind生成的普通的对象相比,可以更加清楚你想要做什么,并进行针对性的优化。

虽说如此,bind怎么实现的还是很trick的,这篇文章就讲一讲bind的实现。

bind的使用

bind的使用分两步,第一步是生成可调用对象,使用你想要bind的东西和需要捕获和延迟绑定的参数调用bind,生成一个新的callable。

std::string s;

auto f = mq::bind(&std::string::push_back, std::ref(s), mq::ph<0>);

这里用的是我自己的实现,bind的第一个参数是你要绑定的callable,这里是一个成员函数,后面的是用来调用的参数,因为是一个成员函数指针,所以参数的第一个应该是一个对象实例,这里是一个引用包装的字符串

std::ref(s)

最后是一个placeholder,他表示对于生成的可调用对象,在调用时第0个参数要被传到这里。这里和标准不一样,标准的placeholder是从1开始的。

使用起来就是这样的

f(‘a‘);
f(‘b‘);

这里用来调用的参数就会被传给绑定进去的push_back的第0个参数。

bind的实现

首先就是bind生成的对象,要做的就是把callable和后面传的参数都丢进一个类里面,这样就构成了一个绑定对象,bind是这么实现的,lambda的内部也是这么实现的。生成的对象叫binder。

template<class TFunc, class... TCaptures>
class binder
{
    using seq = std::index_sequence_for<TCaptures...>;
    using captures = std::tuple<std::decay_t<TCaptures>...>;
    using func = std::decay_t<TFunc>;

    func _func;
    captures _captures;
public:
    explicit binder(TFunc&& func, TCaptures&&... captures)
        : _func(std::forward<TFunc>(func))
        , _captures(std::forward<TCaptures>(captures)...)
    {
    }
    //...

这个实现相当的直接,func就是被绑定的函数,captures是一个tuple,里面装了bind调用时第1个参数后面的所有参数,构造函数把这些东西都forward进去存住。注意所有的类型参数都decay过,这是因为要去掉所有的引用,数组退化成指针,不然没法放进tuple。

而bind,简单点,就是用调用的参数构造binder而已。

template<class TFunc, class... TCaptures>
decltype(auto) bind(TFunc&& func, TCaptures&&... captures)
{
    return detail::binder<TFunc, TCaptures...>{ std::forward<TFunc>(func), std::forward<TCaptures>(captures)... };
}

这里用了C++14的decltype(auto)返回值,这个写法就是通过return语句直接推断返回类型,并且不做任何decay操作。

binder构造好了,下面就是构造它的operator()重载,函数签名也是相当的直接:

    //class binder
    template<class... TParams>
    decltype(auto) operator()(TParams&&... params);
};

接受不定数量的参数而已,这里不同于标准的实现,我没有用任何的SFINAE来做参数的限制,如果调用的参数有错,那么大概会出一大片编译错误。

它的实现是这样的,我把上面binder的实现再复制过来一份一起看

template<class TFunc, class... TCaptures>
class binder
{
    using seq = std::index_sequence_for<TCaptures...>;
    using captures = std::tuple<std::decay_t<TCaptures>...>;
    using func = std::decay_t<TFunc>;

    func _func;
    captures _captures;
public:
    explicit binder(TFunc&& func, TCaptures&&... captures)
        : _func(std::forward<TFunc>(func))
        , _captures(std::forward<TCaptures>(captures)...)
    {
    }

    template<class... TParams>
    decltype(auto) operator()(TParams&&... params);
};

template<class TFunc, class... TCaptures>
template<class... TParams>
decltype(auto) binder<TFunc, TCaptures...>::operator()(TParams&&... params)
{
    return bind_invoke(seq{}, _func, _captures, std::forward_as_tuple(std::forward<TParams>(params)...));
}

这里operator()的实现就是调用的bind_invoke,参数是什么呢,一个index_sequence,之前绑定好的函数和捕获参数,和这里传入的参数列表,参数列表也转发成tuple,为什么要做成tuple呢,因为tuple好用啊,后面就看出来了。

bind_invoke获得了上面这一大坨,它来负责params和_captures正确的组合出来,拿来调用_func。

我们像一下_func应该怎么调用,这里可以使用C++17的invoke,

invoke(_func, 参数1, 参数2, ...)

而这些参数1,参数2,是怎么来的呢,回去看一下调用bind时的captures,如果这个capture不是placeholder,那么这个就是要放进invoke的对应的位置,而如果是placeholder<I>,那么就从params里面取对应的第I个参数放进invoke的位置。

画个图就是这个样子的:

那么,怎么实现这种参数的选择呢,通过包展开

template<size_t... idx, class TFunc, class TCaptures, class TParams>
decltype(auto) bind_invoke(std::index_sequence<idx...>, TFunc& func, TCaptures& captures, TParams&& params)
{
    return std::invoke(func, select_param(std::get<idx>(captures), std::move(params))...);
}

bind_invoke的内部直接调用了标准的std::invoke,传入了func,和后面的select_param包展开的结果,仔细看以下select_param的部分,这里是每个select_param对应一个captures的元素和一整个params tuple

那么select_param的实现大家也基本能猜出来, 对于第一个参数是placeholder<I>的情况,就返回后面的tuple的第I个元素,如果不是,那就返回它的第一个参数。

这里需要注意,select_param是不能用简单的重载的,因为对于

template<size_t I>
void foo(plaecholder<I>)

template<class T>
void foo(T)

这两个重载,是不能正确区分placeholder<I>和其他参数的,需要用SFINAE过滤,而我选择另一种解法,用模板特化,这样更好扩展。

template<class TCapture, class TParams>
struct do_select_param
{
    decltype(auto) operator()(TCapture& capture, TParams&&)
    {
        return capture;
    }
};

template<size_t idx, class TParams>
struct do_select_param<placeholder<idx>, TParams>
{
    decltype(auto) operator()(placeholder<idx>, TParams&& params)
    {
        return std::get<idx>(std::move(params));
    }
};

这是do_select_param的实现(上)和它的一个特化版本(下),特化版本匹配了参数是placeholder的情况。

而select_param函数本身,就是转发对do_select_param的调用而已

template<class TCapture, class TParams>
decltype(auto) select_param(TCapture& capture, TParams&& params)
{
    return do_select_param<TCapture, TParams>{}(capture, std::move(params));
}

这样bind的实现基本上就完结了。还差一个placeholder没提,这个实现也很简单,就是

template<size_t idx>
struct placeholder
{
};

为了方便,使用C++14的变量模板来节省一下平时写placeholder<0>{}的代码

template<size_t idx>
constexpr auto ph = placeholder<idx>{};

那么,bind的实现就基本完结了!

扩展支持嵌套bind

标准的bind是支持嵌套的,比如如下代码

// nested bind subexpressions share the placeholders
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12); // makes a call to f(12, g(12), 12, 4, 5);

嵌套bind也要可以共享调用时的placeholder,这个实现也很简单,只要给上面的do_select_param再增加一个特化,对于参数是binder的类型,嵌套地调用它就好了

template<class TFunc, class... TCaptures, class TParams>
struct do_select_param<binder<TFunc, TCaptures...>, TParams>
{
    decltype(auto) operator()(binder<TFunc, TCaptures...>& binder, TParams& params)
    {
        return apply(binder, std::move(params));
    }
};

这里使用了C++17的apply,就是用tuple的参数包去调用一个函数,如果你的STL还没有实现它,自己去cppreference抄一个实现也行。

至此,bind的实现就完成了,这个实现可以通过cppreference上的所有测试代码,我没有做进一步的测试,如果有错,欢迎在下面评论区指出,谢谢。

时间: 2024-07-28 18:44:20

浅谈std::bind的实现的相关文章

浅谈为什么只有指针能够完成多态及动态转型的一个误区

c++多态由一个函数地址数组Vtable和一个指向Vtable的指针vptr实现. 具体来说,类拥有自己的vtable,类的vtable在编译时刻完成. 每个对象有自己的vptr指针,该指针初始化时指向对象所实现的类的vtable. 关于向上转型的误区: 通常对于向上转型的理解是这样的,当子类对象向上转型(允许隐式)成父类对象时,实际上只是将子类对象暂时看做父类对象,内部的数据并未改变. 对于没有虚函数的对象,这句话是正确的,但是,当引入虚函数后,这样的理解是有问题的,实际上,向上转型的过程中,

浅谈差分约束系统——图论不等式的变形

浅谈差分约束系统——图论不等式的变形 ----yangyaojia 版权声明:本篇随笔版权归作者YJSheep(www.cnblogs.com/yangyaojia)所有,转载请保留原地址! 一.定义 如若一个系统由n个变量和m个不等式组成,并且这m个不等式对应的系数矩阵中每一行有且仅有一个1和-1,其它的都为0,这样的系统称为差分约束( difference constraints )系统. 二.分析 简单来说就是给你n个变量,给m个形如x[i]-x[j]≥k①或x[i]-x[j]≤k②.求两

转 浅谈C++中指针和引用的区别

浅谈C++中指针和引用的区别 浅谈C++中指针和引用的区别 指针和引用在C++中很常用,但是对于它们之间的区别很多初学者都不是太熟悉,下面来谈谈他们2者之间的区别和用法. 1.指针和引用的定义和性质区别: (1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元:而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已.如: int a=1;int *p=&a; int a=1;int &b=a; 上面定义了一个整形变量和一个指针变量p,该指针变量指向a

C++ 浅谈C++中指针和引用

浅谈C++中指针和引用的区别 指针和引用在C++中很常用,但是对于它们之间的区别很多初学者都不是太熟悉,下面来谈谈他们2者之间的区别和用法. 1.指针和引用的定义和性质区别: (1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元:而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已.如: int a=1;int *p=&a; int a=1;int &b=a; 上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单

浅谈C++多态性(转载)

转载:http://blog.csdn.net/hackbuteer1/article/details/7475622 C++编程语言是一款应用广泛,支持多种程序设计的计算机编程语言.我们今天就会为大家详细介绍其中C++多态性的一些基本知识,以方便大家在学习过程中对此能够有一个充分的掌握. 多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念.多态(polymorphisn),字面意思多种形状. C++多态性是通过虚函数来实现的,虚函数允许子

浅谈C语言中的联合体(转载)

联合体union 当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union).在C Programming Language 一书中对于联合体是这么描述的: 1)联合体是一个结构: 2)它的所有成员相对于基地址的偏移量都为0: 3)此结构空间要大到足够容纳最"宽"的成员: 4)其对齐方式要适合其中所有的成员: 下面解释这四条描述: 由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的.为

浅谈文本的相似度问题

今天要研究的问题是如何计算两个文本的相似度.正如上篇文章描述,计算文本的相似度在工程中有着重要的应用, 比如文本去重,搜索引擎网页判重,论文的反抄袭,ACM竞赛中反作弊等等. 上篇文章介绍的SimHash算法是比较优秀的文档判重算法,它能处理海量文本的判重,Google搜索引擎也正是用这 个算法来处理网页的重复问题.实际上,仅拿文本的相似度计算来说,有很多算法都能解决这个问题,并且都达到比 较满意的效果.最常见的几种方法如下 (1)基于最长公共子串 (2)基于最长公共子序列 (3)基于最少编辑距

ACM/ICPC算法训练 之 数学很重要-浅谈“排列计数” (DP题-POJ1037)

这一题是最近在看Coursera的<算法与设计>的公开课时看到的一道较难的DP例题,之所以写下来,一方面是因为DP的状态我想了很久才想明白,所以借此记录,另一方面是看到这一题有运用到 排列计数 的方法,虽然排列计数的思路简单,但却是算法中一个数学优化的点睛之笔. Poj1037  A decorative fence 题意:有K组数据(1~100),每组数据给出总木棒数N(1~20)和一个排列数C(64位整型范围内),N个木棒长度各异,按照以下条件排列,并将所有可能结果进行字典序排序 1.每一

泛型编程与C++标准模板库 : 浅谈sort()排序函数

以前用sort排序的时候,只知道sort函数有如下两种重载方式. template< class RandomIt > void sort( RandomIt first, RandomIt last ); template< class RandomIt, class Compare > void sort( RandomIt first, RandomIt last, Compare comp ); 当时对这些参数也不是很懂,只知道一些简单的用法. 1).比如: 如下代码可以使