一. std::bind

(一)std::bind实现的关键技术

【编程实验】探索bind原理,实现自己的bind函数

#include <iostream>
#include <tuple> using namespace std; //1. 占位符定义
template<size_t idx>
struct placeholder{}; template<size_t idx>
using ph = placeholder<idx>; constexpr ph<> _0{}; //定义一个constexpr类型的占位符对象(_0),并用大括号初始化。
constexpr ph<> _1{}; //定义一个constexpr类型的占位符对象(_1)
constexpr ph<> _2; //定义一个constexpr类型的占位符对象(_2)
constexpr ph<> _3; //定义占位符对象(_3) //2. 参数选择:do_select_param会根据是否为占位符来选择合适的实参。
//2.2 泛化版本:arg不是占位符
template<class Args, class Params>
struct do_select_param
{
decltype(auto) operator()(Args& arg, Params&&) //arg不是占位符,说明arg本身就是一个真正的实参,直接返回。
{
return arg; //由于arg是个引用,decltype(auto)结果也是arg的引用
}
}; //2.3 特化:arg为占位符情况。
template<size_t idx, class Params>
struct do_select_param<placeholder<idx>, Params> //注意Params为bind返回的绑定对象被调用时,传入其中的参数包
{
decltype(auto) operator()(placeholder<idx>, Params&& params) //这里的params是个tuple对象。
{
return std::get<idx>(std::forward<Params>(params));//根据占位符取出params参数包中的实参。
}
}; //2.1 根据占位符选择合适的实参
template<class Args, class Params>
decltype(auto) select_param(Args& arg, Params&& params)
{
//注意,其中的arg是bind绑定时传入的实参,可能是实参或占位符。而params是bind返回的可调用对象被执行时传入的实参。
//如果绑定时是占位符,会do_select_param会分派到其特化的版本,否则分派到其泛化版本。
return do_select_param<Args, Params>{}(arg, std::move(params)); //params是副本,统一用move而不用forwared!
} //3. binder类及辅助函数
//3.3 绑定对象(obj)的调用: 其中args表示传入bind函数的参数,params表示传入obj可调用对象的参数。
template<size_t... idx, class Callable, class Args, class Params>
decltype(auto) bind_invoke(std::index_sequence<idx...>, Callable& obj, Args& args, Params&& params)
{ //根据args是否是占位符来选择合适的实参,并传给可调用对象obj。
//注意:为了提高效率,参数是以move的形式(右值)被传递给obj可调动对象的。
return std::invoke(obj, select_param(std::get<idx>(args), std::move(params))...);//C++17, invoke(func, 参数1, 参数2, ...)
} //3.2 binder类(核心类)
template<class Callable, class... Args>
class binder
{
using Seq = std::index_sequence_for<Args...>; //等价于std::make_index_sequence<sizeof...(Args)>
//会创建类似一个index_sequence<0,1,2...>的类
//保存bind函数的所有实参(即可调用对象及其实参)
using args_t = std::tuple<std::decay_t<Args>...>; //注意,decay_t去掉其引用、const\volatile等特性)
using callable_t = std::decay_t<Callable>; //可调用对象的类型(注意,decay_t去掉其引用、cv等特性) callable_t mObj; //以副本的形式保存可调用对象
args_t mArgs; //以副本的形式保存可调用对象的所有实参。(打包放在tuple中)
public: //注意,不管是可调用对象(callableObj),还是它的实参(args)均会根据其左右值特性,被复制或移动到Binder类中,以副本的形式保存起来。
explicit binder(Callable&& callableObj, Args&& ... args)
:mObj(std::forward<Callable>(callableObj)),mArgs(std::forward<Args>(args)...)
{
} //Binder仿函数的调用
template<class... Params>
decltype(auto) operator()(Params&& ... params) //可调用对象被调用,并传入参数。
{
//第1个参数:升序的整数序列。第4个参数将传入的params实参转化为tuple对象。
//注意:std::forward_as_tuple被定义成tuple<_Types&&...>(_STD forward<_Types>(_Args)...);
// 这说明params将以引用的形式被保存在tuple中!!!
return bind_invoke(Seq{}, mObj, mArgs, std::forward_as_tuple(std::forward<Params>(params)...));
}
}; //3.1 bind辅助函数
template <class Callable, class... Args>
decltype(auto) bind(Callable&& callableObj, Args&& ... args)
{
return binder<Callable, Args...>(std::forward<Callable>(callableObj), std::forward<Args>(args)...);
} //测试函数
int add(int x, int y)
{
return x + y;
}
//测试类
class Test
{
public:
int x;
int add(int x, int y)
{
return x + y;
}
}; int main()
{
// 自定义bind函数的测试
auto f1 = bind(add, , ); //add为函数名,会自动转为函数指针类型。
cout << f1() << endl; // auto f2 = bind(add, _0, _1);
cout << f2(, ) << endl; // auto lam = [](int x, int y) {return x * y; };
auto f3 = bind(lam, _0, );
cout << f3() << endl; // int a = ;
Test t1;
auto f4 = bind(&Test::add, &t1, _0, _1); //add函数的第1个参数是this指针,因此需要将&t1传进去。同时注意add是成员函数,
//需要用&来创建指向成员的函数指针。
cout << f4(a, ) << endl; //13。 a和3都是以引用的形式传入“绑定对象”f4中。 t1.x = ;
auto f5 = bind(&Test::x, t1); //传入t1的副本
auto f6 = bind(&Test::x, std::ref(t1)); //传入t1的引用 t1.x = ; cout << f5() << endl; //
cout << f6() << endl; // return ;
}

std::bind的模拟实现

1. 保存可调用对象及其形参

  (1)保存可调用对象的实例:bind时会将可调用对象(如func)作为binder类的一个成员变量(如mObj)保存起来。

  (2)保存形参:由于形参是变参,不能直接将变参作为成员变量。这里可用tuple将变参打包并保存起来(如mArgs)。

  2. 可调用对象的执行

  (1)将tuple展开为变参:绑定可调用对象时,是将可调用对象的形参(可能含占位符)保存在tuple中。到了调用阶段,就必须反过来将tuple展开为可变参数。因为这个可变参数才是可调用对象的形参。这里借助一个整型序列来将tuple变为可变参数,在展开tuple的过程中还需要根据占位符来选择合适实参,即占位符要替换为调用实参。

  (2)根据占位符来选择合适的实参(如select_param函数)

     ①tuple中可能含有占位符,如果发现某个元素类型为占位符,则从调用时生成的实参tuple(如params)中取出相应的元素,用来作为变参的一个参数。如上面的select_param(ph<0>,{4,5,6}),ph<0>是个占位符,表示该处的实参是其后的{4,5,6}这个tuple中的0位置元素,即4。(具体的实现见do_select_param特化版本)

    ②如果某个类型不为占位符时,则直接从绑定时生成的形参tuple(如mArgs)中出取参数,用来作为变参的一个参数。如select_param(1,{4,5,6}),由于第1个实参为1,不是占位符,因此直接将1这个实参取出,传入invoke函数(具体实现见do_select_param泛化版本)

(二)注意事项

  1.bind函数的所有实参(含第1个实参)都是按值传递的,即它们均通过复制或移动的方式以副本的形式保存起来的。可以通过对实参实施std::ref的方式来达到按引用存储的效果

  2.bind的返回值(称之为“绑定对象”)的所有实参都是按引用传递的,因为这些参数被打包成std::forward_as_tuple,而这里存的都是引用。【见binder的operator()函数】

  3. 对于事先绑定的参数需要传具体的变量或值进去。对于不事先绑定的参数,需要传递占位符(如_1)进去。

  4. 绑定类的成员函数时,需要用&来创建成员函数指针再将其作为实参传给bind的第1个实参,同时在bind的第2个实参中为该成员函数指定this指针,然后再传入该成员函数的其它参数。

二. 优先使用lambda而非bind

(一)lambda表达式具有更强的可读性,表达力更好。

(二)bind函数绑定重载函数时会遇到二义性问题。由于函数名只是个名字,不带形参类型等其他附加信息,所以无法知道被绑定的是哪个重载版本的函数。

(三)lambda表达式可能生成比使用std::bind运行效率更高的代码。编译器可能对函数名做inline函数调用,而不太可能对函数指针做这种优化。如setAlarm函数在lambda中的调用,可以被内联。但是std::bind绑定的是setAlarm函数指针而不是函数体本身,所以无法被内联

(四)lambda的传参方式(按值还是按引用)比bind函数更直观。Lambda表达式的传参方式只需看其声明就可知。但使用bind需要牢记其工作原理,即bind在绑定时是按值传递实参的,绑定对象在调用时是按引用传递实参的

(五)自C++14以后,std::bind己经彻底失败用武之地,可以完全被lambda替代。

三、bind使用的两种场合(C++11中)

(一)移动捕获:C++11中的lambda没有提供移动捕获特性,但可以通过std::bind和lambda表达式来模拟移动捕获(见《第18课 捕获机制及陷阱》)。而C++14中提供了初始化捕获的语言特性,从而消除了如此模拟的必要。

(二)多态函数对象:因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何类型的实参。当想要绑定的对象具有一个函数运算符模板时,是有利用价值的。但在C++14中,使用带有auto类型形参的lambda可以轻而易举地达成同样的效果。

【编程实验】lambda与bind的pk

#include <iostream>
#include <functional>
#include <chrono> using namespace std;
using namespace std::chrono; //for steady_clock
using namespace std::literals; //for 1h, 30s等
using namespace placeholders; //for _1、_2等。 using Time = std::chrono::steady_clock::time_point; //表示时刻的类型
using Duration = std::chrono::steady_clock::duration; //表示时长的类型
enum class Sound{Beep, Siren, Whistle}; //警报的声音类型(哔哔声、汽笛声、口哨声)
enum class Volume{Normal, Loud, LoudPlusPlus}; //音量大小 //警报函数(3个形参)
void setAlarm(Time t, Sound s, Duration d) //在t时刻,发出声音s,持续时长d
{
} //警报函数(4个形参)
void setAlarm(Time t, Sound s, Duration d, Volume v)
{
} class Widget{};
enum class CompLevel{Low, Normal, High}; //压缩等级
Widget compress(const Widget& w, CompLevel lev) //制作w的压缩副本
{
return Widget();
} class PolyWidget
{
public:
template<typename T>
void operator()(const T& param){} //仿函数可接受任何类型的形参对象,被称为多态函数对象!
}; int main()
{
//1. lambda比bind可读性更强(下列between函数用于判断实参是否介于极小值和极大值之间)
const auto lowVal = ;
const auto highVal = ;
//1.1使用lambda
auto betweenL = [lowVal, highVal](const auto& val) //C++14
{
return lowVal <= val && val <= highVal;
}; //1.2 使用bind
auto betweenB = std::bind(std::logical_and<>(),
std::bind(std::less_equal<>(), lowVal, _1),
std::bind(std::less_equal<>(), _1, highVal)); int x = , y = ;
cout << betweenL(x) << endl; //
cout << betweenB(x) << endl; //
cout << betweenL(y) << endl; //
cout << betweenB(y) << endl; //0 //2. bind在延时求值、识别重载函数的使用方式繁琐。此外绑定函数时不会被内联到operator()中
//2.1 lambda表达式(C++14)
auto setSoundL = [](Sound s) //setSoundL后面的L表达lambda表达式
{
setAlarm(steady_clock::now() + 1h, s, 30s); //C++14支持1h、30s的写法。调用3个形参的setAlarm,不会发生二义性!
//由于setAlarm是常规的函数调用,编译器会考虑将setAlarm内联到lambda中
};
setSoundL(Sound::Beep); //2.2 使用bind
//2.2.1 错误的用法(警报启动时刻是在调用std::bind函数之后的1个小时)
//auto setSoundB = std::bind(setAlarm, //setSoundB中的"B"表示bind
// steady_clock::now() + 1h, //错误!该参数传给bind,而非setAlarm。意味着调用bind时时刻值己求出。但我们期望是setAlarm调用时对时刻求值。
// _1, //将传给setSoundB的第1个参数传到setAlarm的第2个参数中。此处bind无法知道该实参真正的类型,需向setAlaram查询!
// 30s);
//2.2.2 正确的用法(警报启动时刻是在调用setAlarm函数之后的1个小时)
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm), //setAlarm是个函数名。没有形参信息,不知调用哪个重载版本的setAlarm,需强转!
std::bind(std::plus<>(), std::bind(steady_clock::now), 1h), //延时求值,注意这里传入的是now这个函数,而不是now()的返回值!
_1,
30s
);
setSoundB(Sound::Beep); //由于bind参数中的setAlarm是个函数指针,故setSoundB的operator()中无法将setAlarm函数体本身内联进来。 //3. bind的传参方式不直观
Widget w;
//3.1 在bind调用时的传参比较
auto compressRateB = std::bind(compress, w, _1); //bind时w被按值传递给绑定对象,是以副本形式被存储。(但这一点从bind的形参中看不出来,只有牢记其工
//作原理才能知道这个事实。 auto compressRateL = [w](CompLevel lev) {return compress(w, lev); }; //使用lambda时非常直观,w和lev都是以按值方式被传递给lambda表达式。 //3.2 在绑定对象被调用时传参的比较
compressRateB(CompLevel::High); //bind返回的绑定对象调用时,实参都是通过按引用传递的(这点也是要牢记bind工作原理才会知道的事实)
compressRateL(CompLevel::High); //参数是以按值方式传递的。 //4. 多态函数对象
PolyWidget pw;
//4.1 C++11中,bind的用途。绑定多态仿函数对象
auto pwB = std::bind(pw, _1);
pwB(); //传入int
pwB(nullptr); //传入nullptr
pwB("SantaClaus"); //传入字符串字面量,pw.operator()中可以传入各种类型的对象。 //4.2 C++14中,可利用带auto形参的lambda来达到类似的效果
auto pwL = [&pw](const auto& param)
{
pw(param);
}; pwL(); //传入int
pwL(nullptr); //传入nullptr
pwL("SantaClaus"); //传入字符串字面量,pw.operator()中可以传入各种类型的对象。 return ;
}

第19课 lambda vs std::bind的更多相关文章

  1. 第12课 std::bind和std::function(3)_std::function可调用对象包装器

    1. std::function (1)首先是一个类模板,用于包装可调用对象.可以容纳除了类成员(函数)指针之外的所有可调用对象. (2)可以将普通函数,lambda表达式和函数对象类统一起来.尽管它 ...

  2. C++ 中std::function 、std::bind的使用和lambda的使用

    std::function是可调用对象的包装器:std::bind是将可点用对象和其参数一起进行绑定,且绑定后的结果可以使用std::function对象进行保存,并延迟调用到需要调用的时候: 在C+ ...

  3. C++11 std::function、std::bind和lambda表达式

    参考博客: C++可调用对象详解-https://www.cnblogs.com/Philip-Tell-Truth/p/5814213.html 一.关于std::function与std::bin ...

  4. 第11课 std::bind和std::function(2)_std::bind绑定器

    1. 温故知新:std::bind1st和std::bind2nd (1)bind1st.bind2nd首先它们都是函数模板,用于将参数绑定到可调用对象(如函数.仿函数等)的第1个或第2个参数上. ( ...

  5. 第17课 lambda表达式

    一. lambda表达式 (一)语法定义:[capture](paramters) mutable ->returnType{statement} 1.[capture]:捕获列表 (1)lam ...

  6. std::bind接口与实现

    前言 最近想起半年前鸽下来的Haskell,重温了一下忘得精光的语法,读了几个示例程序,挺带感的,于是函数式编程的草就种得更深了.又去Google了一下C++与FP,找到了一份近乎完美的讲义,然后被带 ...

  7. std::bind和std::function

    std::bind 用于绑定一个函数,返回另外一种调用方式的函数对象 ,可以改变参数顺序 和个数,特别是在多线程的程序中,经常用它将函数进行包装,然后打包发送给工作线程,让工作线程去执行我们的任务. ...

  8. C++11 std::bind std::function 高级使用方法

    从最基础的了解,std::bind和std::function /* * File: main.cpp * Author: Vicky.H * Email: eclipser@163.com */ # ...

  9. std::bind技术内幕

    引子 最近群里比较热闹,大家都在山寨c++11的std::bind,三位童孩分别实现了自己的bind,代码分别在这里: 木头云的实现 mr.li的实现 null的实现,null的另一个版本的实现 这些 ...

随机推荐

  1. 一个JAVA应用启动缓慢问题排查 --来自jdk securerandom 的问候

    开发某个项目过程中,就需求,搭建了一套测试环境.很快完成! 后来代码中加入了许多新功能,会涉及到反复重启,然后就发现了启动特别慢.这给原本功能就不多的应用增添了许多的负担. 我决定改变这一切!找到启动 ...

  2. C# 方法的out、ref、params参数

    一.out参数实例 [实例]求一个数组中的最大值.最小值.总和.平均值 class Program { static void Main(string[] args) { //写一个方法 求一个数组中 ...

  3. 微信测试号:config:invalid url domain

    今天调试微信分享的时候,配置参数时一直提示config:invalid url domain,网上找了一下,都说是appId和域名没有绑定.仔细看了下,有绑定没错.又猜测是不是二级域名的问题,因为是测 ...

  4. python 排序 拓扑排序

    在计算机科学领域中,有向图的拓扑排序是其顶点的先行排序,对于每个从顶点u到顶点v的有向边uv,在排序的结果中u都在v之前. 如果图是有向无环图,则拓扑排序是可能的(为什么不说一定呢?) 任何DAG具有 ...

  5. C/C++中new的使用规则

    本人未重视new与指针的使用,终于,终于在前一天船翻了,而且没有爬上岸: 故此,今特来补全new的用法,及其一些规则: 话不多说 C++提供了一种“动态内存分配”机制,使得程序可以在运行期间,根据实际 ...

  6. XGBoost 完整推导过程

    参考: 陈天奇-"XGBoost: A Scalable Tree Boosting System" Paper地址: <https://arxiv.org/abs/1603 ...

  7. liteos互斥锁(七)

    1. 概述 1.1 基本概念 互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对共享资源的独占式处理. 任意时刻互斥锁的状态只有两种,开锁或闭锁.当有任务持有时,互斥锁处于闭锁状态,这个任务 ...

  8. 编译安装redis 3.2.9 make test 时报错

    默认监听端口:6379(可以创建多个端口的配置文件) 源码安装: $ yum install tcl $ wget http://download.redis.io/releases/redis-3. ...

  9. git使用笔记(第一次)

    背景:公司基于微服务的架构,前端的服务web只有一个.在并行完成不同需求的测试任务时,该服务会拉出不同分支,此时会碰到sit环境与其他测试小伙伴部署冲突的问题.解释下.需求1对应的服务web的A分支, ...

  10. [原创]python+beautifulsoup爬取整个网站的仓库列表与仓库详情

    from bs4 import BeautifulSoup import requests import os def getdepotdetailcontent(title,url):#爬取每个仓库 ...