一、包装器

1.1 什么是包装器

包装器是对已有对象进行再次封装,使其拥有新的行为或接口。

例如:指针包装成智能指针,函数包装成函数对象,引用包装成对象。

核心思想:原始对象 -> 经过包装器包装 -> 获得新的能力

1.2 为什么需要包装器

举几个经典问题:

问题1

模板无法直接存储引用,vector<int&> 非法,于是出现了 reference_wrapper<int> 。

问题2

不同类型函数无法统一存储

对于普通函数、Lambda、仿函数、成员函数它们的类型完全不同,需要 std::function 统一包装。

问题3

参数顺序固定,func(a, b, c) 想提前绑定部分参数,需要 std::bind 。

二、reference_wrapper

2.1 为什么引用不能放进容器

引用的特点:必须初始化,不可重新绑定。对于 vector<int&> 无法实现,是因为 vector 不是一个个对象尾插的,它底层是2倍或1.5倍扩容机制,这就导致了vector多扩容的部分对象无法初始化。假如不存在扩容机制,只是一个个扩容,那么对于 v.push_back(a);  v.back() = c; 这份代码意味着,v 的最后一个元素引用了a对象,但是 v.back() = c; 意味着将 c 的值赋值给了 v 的最后一个元素,不是引用重新绑定了 c 对象。由于vector底层有时扩容会释放旧空间,开辟新空间,这一点也是无法解决的。

2.2 reference_wrapper 的出现

对于上述问题,C++98的解决方案是容器不存引用,而是存指针。

vector<int*> v;
v.push_back(&a);
v.push_back(&b);

这样做虽然可以达到目的,但是使用不方便,如 (*v[0])++; 需要解引用。

于是标准库提供了 

template <class T> class reference_wrapper;

它存在于 <functional> 这个头文件中。

它的本质就是:引用的对象化,把引用包装成一个类,底层原理就是包装指针的类。

示例:

#include <iostream>
#include <functional>
using namespace std;
int main()
{
    int a = 0;
    reference_wrapper<int> ra = a;
    ra.get() = 100;
    cout << a << endl;
    return 0;
}

2.3 ref的使用

在实际开发中几乎不会这样写

reference_wrapper<int> ra = a;

而是

ref(a)

例如:

int a = 10;

auto rw = std::ref(a);

ref 就是一个构造 reference_wrapper对象的函数。

学到这里,就能解决容器中如何存引用的问题。

#include <iostream>
#include <vector>
#include <functional>
using namespace std;
int main()
{
    vector<reference_wrapper<int>> v;
    int a = 10;
    v.push_back(ref(a));
    cout << v.back().get() << endl;
    int b = 20;
    v.back() = b;
    cout << v.back() << endl;
    return 0;
}

对于 reference_wrapper 和 ref 还有很多其他用途,例如解决 thread 和 bind 传引用的问题。

三、bind

3.1 什么是bind

std::bind 是一个函数模板,用于生成可调用对象的包装器(函数适配器)。它接收一个可调用对象,并根据参数绑定规则(值绑定、引用绑定、占位符等)生成一个新的可调用对象。

该新可调用对象可以对原函数的参数进行绑定、重排或部分固定,从而改变函数的调用形式。

基本语法:auto newCallable = bind(callable, arg_list); 

其中 newCallable 本身是一个可调用对象,用来接受bind返回的可调用对象,callable 是bind处理的目标对象,arg_list 是参数绑定规则。

3.2 占位符

arg_list 中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示 newCallable的参数,它们占据了传递给 newCallable 的参数位置。数值n表示生成的可调用对象中参数的位置:_1 为newCallable的第一个参数,_2 为第二个参数,以此类推,_n 为第n个参数。

这些占位符存放在了placeholders的一个命名空间中。

3.3 bind 的使用

// 基本用法:绑定函数 + 固定参数
int add(int a, int b)
{
    return a + b;
}
auto f = std::bind(add, 10, 20);
cout << f();  // 30

这里发生的是:add 函数经过 bind 函数的处理,返回一个新的可调用对象,其中可调用对象的第一个参数被固定为 10,第二个参数被固定为 20,所以 f 就不需要传递参数,也不能传递参数。

// 使用占位符
using namespace std::placeholders;
auto f = std::bind(add, 10, _1);
f(5);

调用 f(5); 等价于add(10, 5);

只需记住一句话,_1 表示:调用时再传进来的第一个参数。其中_1 跟add没有关系,它不是代表add的第一个参数。

// 参数重排
auto f = std::bind(add, _2, _1);
f(10, 20);

调用 f(10, 20); 等价于 add(20, 10);

因为_1 是f(10,20) 的第一个参数,_2 是f(10, 20)的第二个参数。

// 忽略参数
auto f = std::bind(add, _1, 100);
f(5);

调用 f(5); 等价于 add(5, 100);

// 绑定成员函数
struct Test 
{
    int add(int x) 
    {
        return x + 10;
    }
};
Test t;
auto f = std::bind(&Test::add, &t, _1);

调用 f(5); 等价于 t.add(5);

对于成员函数,它们的参数列表会隐含地多一个this指针指向要调用成员函数的对象,所以必须要传一个对象的地址。

// bind + ref
int x = 10;
auto f = std::bind(add, std::ref(x), _1);

作用:对于bind默认生成的可调用对象,都是传值传参,但对上述代码来说,不是拷贝 x 而是引用x,可以修改 x 的值。

3.4 bind 底层原理

bind 返回的到底是什么?

bind 返回的是一个匿名函数对象(仿函数)。

可以理解为它内部生成了类似的类

class BindObject
{
    FunctionType func;     // 原函数
    tuple stored_args;     // 绑定的参数
public:
    template<class... CallArgs>
    auto operator()(CallArgs&&... call_args)
    {
        return invoke(func, stored_args, call_args...);
    }
};

bind的核心结构可以拆成 3 层

第一层:保存可调用对象 (普通函数、lambda、成员函数、函数对象)

第二层:保存参数绑定规则 (值绑定、引用绑定、占位符)

第三层:调用时展开参数 (依次给可调用对象传递保存的参数)

3.5 bind 的缺点

1. 可读性差

bind(add, _2, _1) 

对于不明白的人,跟可能认为 _1 为add的第一个参数,_2 为add的第二个参数,不直观

2. 错误信息难看

bind为一个函数模板,且返回一个新的可调用对象,这个过程是十分复杂的,出错了不容易排查

3. lambda 可以完全替代

实际场景中,没有人会用bind来调整函数参数顺序,都是用来固定某个参数,对于lambda也可以很简洁地完成该功能。

auto f = [](int x){
    return add(10, x);
};

四、function

4.1 什么是function

function是一个类模板,也是一个包装器。function实例化出的对象可以包装并存储可调用对象,包括函数对象、仿函数、lambda、bind表达式、成员函数、普通函数等,存储的可调用对象被称为function的目标。若function不含目标,则它为空,调用空function的目标会抛出bad_function_call 异常。

它也被定义在<functional>的头文件中。

它主要的功能是:把不同类型的可调用对象统一成一个类型

基本语法:function<返回值(参数)> f;

4.2 function 的使用

普通函数

int add(int x, int y) { return x + y; }
std::function<int(int, int)> f1 = add;
f1(2,5);

lambda

std::function<int(int, int)> f2 = [](int x, int y) { return x + y; };
f2(3, 4);

仿函数

struct Functor 
{
    int operator()(int x, int y) 
    {
        return x + y;
    }
};
std::function<int(int, int)> f3 = Functor();
f3(1, 3);

bind表达式

std::function<int(int, int)> f4 = std::bind(add, 10, std::placeholders::_1);
f4(2);

最常用的场景之一

对于对某些特定的指令,执行特定函数。

// function作为map的映射可调用对象的类型​
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int x, int y){return x + y;}},
{"-", [](int x, int y){return x - y;}},
{"*", [](int x, int y){return x * y;}},
{"/", [](int x, int y){return x / y;}}
};

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章