std::bind()与std::ref()

1.std::bind()

1. 前言

不知道大家在做项目写程序的过程中有没有遇到这样的情况,别的模块类提供了一个拥有很多参数接口函数,但是我这个功能只用到其中几个,其他的参数都是固定的,可是为了调用这个接口函数,不得不将所有的参数写一遍,每次写一堆固定参数都感觉在浪费生命。

有的人可能想到默认参数,的确,默认参数可以解决部分问题,因为默认参数只能出现参数列表的尾部,如果4个参数中,我需要传递的参数是第4个,而前3个参数想默认的话,默认参数是做不到这种效果的,并且别人的接口函数也不一定会有默认参数。

函数封装,这是一个办法,我们在自己的模块中添加一个对接口函数进行包装后的函数,将不变的参数进行固定,然后只留可变的参数供我们自己调用,如果我们有3种常用的调用方式可能就需要定义3个函数,这种方法可行,不过比较麻烦,而std::bind()函数就是为了包装函数而生的,使用起来更加方便。

2. std::bind()的作用

std::bind()的作用就是对原函数进行包装,可以将参数值固定,调换顺序然后生成新的函数供我们调用。举个例子,一块铁片你可以拿它来做很多事情,打造一下可以做成一把刀,敲敲打打可以做成一个桶,甚至直接拿来就可以挡窗户上的洞。std::bind()的作用就是把这块铁的作用固定,比如给她安上一个刀把,这样我们每次使用就可以把这块铁片当成菜刀来使用了。

std::bind()可以包装各种函数,但是这篇文章只总结一下包装普通函数的方法,因为在学习的过程中我发现单单是包装普通函数也会遇到很多问题,所以为了列举出诸多可能,说明各种注意事项,本文还是只关注于普通函数的包装,至于成员函数的包装还是放到以后的文章,给自己埋下一个坑。

在包装普通函数时,std::bind()第1个参数就是原函数的名字,当然也可以是指向函数的指针,或者函数引用,从第2个参数开始,填写的内容依次对应原函数中的各个参数,所以说如果原函数是3个参数,如果想包装它,那么std::bind()需要传入4个参数,如果原函数是8个参数,那么包装它的std::bind()就需要传入9个参数,这里为了将原函数和包装后的函数参数建立联系,需要引入命名空间std::placeholders。

3.placeholders的作用

std::placeholders的命名空间下有多个参数占位符,比如placeholders::_1、placeholders::_2等等,最大为placeholders::_20,在包装普通函数时,固定的参数很好说,就是填写固定值就可以,但是要想原函数的参数和包装后函数的参数建立联系就需要用到刚刚提到的占位符, placeholders::_1就表示包装后函数的调用时的第1个参数,同理placeholders::_2就表示包装后函数的调用时的第2个参数

有了占位符的概念,我们就可以推断出,包装后的函数与原函数相比,不但可以减少函数参数,也可以增加函数参数,虽然暂时没有想到什么实际的使用场景,但是理论上是可行的。

4.std::bind()使用测试

首先需要先引入头文件,免得找不到命名空间和函数定义

#include <iostream>
#include <functional>
using namespace std;

4.1 固定参数、调换顺序

void func1(int n1, int n2, int n3)
{
    cout << n1 << ' ' << n2 << ' ' << n3  << endl;
}

void test1_1()
{
    auto f1 = std::bind(func1, placeholders::_1, 101, placeholders::_2);
    f1(11, 22);   // same as call func1(11, 101, 22)
}

void test1_2()
{
    auto f1 = std::bind(func1, placeholders::_2, 101, placeholders::_1);
    f1(11, 22);   // same as call func1(22, 101, 11)
}

// 输出
//11 101 22
//22 101 11

函数test1_1()展示了std::bind()函数最常见的用法,其中参数n2被固定为101,参数n1使用占位符placeholders::_1表示,表示包装后函数的第1个参数会传给形参n1使用,同理包装后函数的第2个参数会传给形参n3使用,所以调用函数f1(11, 22) 就等同于调用函数 func1(11, 101, 22),test1_2()函数简单展示了调换参数顺序的方法,只要明白了placeholders的作用,这两个例子也就明白了。

4.2 普通函数的参数中有引用类型

弄明白上面的例子之后,可能会产生一种我会了的错觉,想象一下如果原函数参数中包含引用类型应该怎样写,可以自己先想一下,然后看看下面的例子

void func4(int n1, int n2, int& n3)
{
    cout << n1 << ' ' << n2 << ' ' << n3 << endl;
    n3 = 101;
}

void test4_1()
{
    int n = 10;
    auto f4 = std::bind(func4, 11, 22, n);
    n = 33;
    f4();   // same as call func4(11, 22, 10)
    cout << "n = " << n << endl;
}

void test4_2()
{
    const int n = 30;
    auto f4 = std::bind(func4, 11, 22, n);
    f4();   // same as call func4(11, 22, 30)
}

void test4_3()
{
    int n = 30;
    auto f4 = std::bind(func4, 11, 22, ref(n));
    n = 33;
    f4();   // same as call func4(11, 22, n)
    cout << "n = " << n << endl;
}

void test4_4()
{
    const int n = 30;
    auto f4 = std::bind(func4, 11, 22, ref(n));
    //f4();   // 编译错误,无法将参数 3 从“const int”转换为“int &”
}

// 输出
//11 22 10
//n = 33
//11 22 30
//11 22 33
//n = 101

如果能准确说出test4_1()函数的输出结果,那么后面的内容应该是不需要看了,如果只回答对了部分内容,或者干脆全错了,那么我们还有很长的路要走。

std::bind()的官方文档中有这样一句话,std::bind()函数中的参数在被复制或者移动时绝不会以引用的方式传递,除非你使用了std::ref()或者std::cref()包装的参数,如果知道了这个限定,就很容易明白函数test4_1()函数的输出结果了。

在函数test4_1()中std::bind(func4, 11, 22, n)就相当于std::bind(func4, 11, 22, 10),所以输出结果为11 22 10,可是函数func4()中还有一句 n3 = 101;,这就很让人奇怪了,我们知道常数是没办法作为参数传递给可变引用变量的,如果说把10作为参数传递给参数int& n3肯定会报错,而函数test4_1()却正常执行,没有任何问题。

我们猜测常数10到参数int& n3并不是直接传递,而是发生了拷贝,而函数func4()中修改的n3变量也是修改的拷贝内容,所以我们做了test4_2()这个实验,发现将变量n改为常量也是可以正常执行的,甚至直接写成std::bind(func4, 11, 22, 10)也是没问题的,这也验证了我们上面的想法。

既然文档了提到了std::ref()和std::cref()函数,那么我们想传递引用给原函数只能使用它们了,看下函数test4_3()的实现,这才是正确传递引用变量的方式,变量n被函数 std::ref() 包装之后,既能够感受到本函数中变量n的变化,也能够传入到原函数中被原函数的逻辑改变,并将结果反映回来。

函数test4_4()只是一个常量传递的简单测试,将一个常量作为可变引用变量来传递肯定是无法通过编译的,这在函数调用时很明确,但是在std::bind()加入之后显得有些混乱,只要记住一点,常量不应该被改变,如果传递之后内容可能会变化,那么很可能这种写法就是错误的。

4.3 普通函数嵌套包装

实际上就是普通函数包装的变形和组合,直接写个例子吧,如果test1_1()、test1_2()、test1_3()三个函数的输出结果都答对了就说明已经掌握了。

void func(int n1, int n2, int n3)
{
    cout << n1 << ' ' << n2 << ' ' << n3 << endl;
}

int calc_value(int c1)
{
    return c1 * c1;
}

void calc_value2(int c1)
{
    int result = c1 * c1;
}

void test1_1()
{
    auto f1 = std::bind(func, placeholders::_1, 101, std::bind(calc_value, placeholders::_2));
    f1(11, 2);   // same as call func(11, 101, calc_value(2))
}

void test1_2()
{
    int n = 2;
    auto f1 = std::bind(func, placeholders::_1, 101, std::bind(calc_value, std::ref(n)));
    n = 4;
    f1(11, 2);   // same as call func(11, 101, calc_value(44)) 多出的参数2无人使用
}

void test1_3()
{
    auto f1 = std::bind(func, placeholders::_1, 101, std::bind(calc_value2, placeholders::_2));
    //f1(11, 2);   // 编译出错,无法将参数 3 从“void”转换为“int”
}

// 11 101 4
// 11 101 16

第一个test1_1函数的逻辑应该很容易理解,就是把函数calc_value(2)的返回值作为函数func的第三个参数,而函数test1_2中利用了std::ref()传递引用的功能,将变量n作为引用变量进行传递,在包装调用之前可以感知到参数n的变化。

其实难点在第三个函数test1_3,可能大家知道这里会报错,因为我们需要返回值但是却包装了一个没有返回值的函数,但其实把第二行注释掉之后,程序就可以成功编译,也就是说包装错误的函数如果不被调用,是不会报错的,这一点和模板函类不使用就不会创建很相似,最终是相同的。

5.包装类成员

在深入学习std::bind()这个函数之前一直以为它只能用来包装函数,后来通过进一步了解发现它还能用来包装成员变量,我们一起来看一下简单的实现方法。
成员函数的包装

这里我们不考虑静态成员函数,因为静态函数没有this指针,和普通的函数基本一样,在用法上也没有很大的差异,所以此处的包装只考虑成员非静态函数,可以尝试分析以下几个例子。

class CTest
{
public:
    CTest() {}
    ~CTest() {}
public:
    void    func1(int n1, int n2)
    {
        cout << "func1 " << n1 << ' ' << n2 << endl;
    }

    int     n_public;

private:
    void    func2(int n1, int n2)
    {
        cout << "func2 " << n1 << ' ' << n2 << endl;
    }

    int n_private;
};


void test2_1()
{
    CTest testObj;
    auto f2 = std::bind(&CTest::func1, testObj, 101, placeholders::_1);
    f2(1);   // same as call testObj.func1(101, 1)
}

void test2_2()
{
    CTest testObj;
    auto f2 = std::bind(&CTest::func1, &testObj, 101, placeholders::_1);
    f2(2);   // same as call testObj.func1(101, 2)
}

void test2_3()
{
    CTest testObj;
    CTest& obj = testObj;
    auto f2 = std::bind(&CTest::func1, obj, 101, placeholders::_1);
    f2(3);   // same as call testObj.func1(101, 3)
}


void test2_4()
{
    CTest testObj;
    auto f2 = std::bind(&CTest::func1, placeholders::_1, placeholders::_2, 101);
    f2(testObj, 4);   // same as call testObj.func1(4, 101)
}

void test2_5()
{
    CTest testObj;
    // auto f2 = std::bind(&CTest::func2, &testObj, 101, placeholders::_1);
    // 编译错误,func2不可访问
}

//func1 101 1
//func1 101 2
//func1 101 3
//func1 4 101

前三个函数tes2_1()、tes2_2()、tes2_3()的作用基本一致,就是将一个类的非静态成员函数和对象绑定,并且可以动态绑定一些参数,三种调用方式都可以,暂时没有发现什么问题,大家知道区别的可以指导我一下,我补充上来,需要注意的是函数std::bind()参数个数需要在原函数参数个数的基础上加两个,第一个很明显就是函数名,而第二个必须是调用这个函数的对象,至于传递的是指针还是引用都没有什么问题,这两个参数过后才是真正的原函数的参数。

函数test2_4()相对于前三个来说更加灵活,将对象也最为参数在调用时传入,这就相当于把一个成员函数看成,一个普通函数然后在第一个参数前加this指针的形式,后面这种调用方式在查看C++调用堆栈时应该很容易看到,本质上是一样,其实这里还有一个对象传递的问题,我们在成员变量时再测试一下。

函数test2_5()出现了编译错误,原因是在使用函数std::bind()的时候也要考虑到原函数的访问权限,在测试函数中访问对象的私有函数显然是不可以的。

5.1 成员变量的包装

void test3_1()
{
    CTest testObj;
    auto f3 = std::bind(&CTest::n_public, testObj);
    f3(1) = 10;
    cout << f3(1) << endl;
    cout << testObj.n_public << endl;
}


void test3_2()
{
    CTest testObj;
    auto f4 = std::bind(&CTest::n_public, placeholders::_1);
    f4(testObj) = 4;
    cout << f4(testObj) << endl;
    cout << testObj.n_public << endl;
}


void test3_3()
{
    CTest testObj;
    auto f3 = std::bind(&CTest::n_public, std::ref(testObj));
    f3(1) = 11;
    cout << f3(1) << endl;
    cout << testObj.n_public << endl;
}

//10
//-858993460
//4
//4
//11
//11

这个成员变量的绑定测试结果,有没有让人意想不到呢?或者说这种f3(1) = 10;写法已经让人很惊讶了,其实我在写例子的时候就是简单试试,没想到这样写居然可以,看起来好像把一个值赋值给了一个函数一样。

函数test3_1()的第二个输出可能有点想不到,但是看到结果是有些人可能就明白了,因为在上一篇里提到“std::bind()函数中的参数在被复制或者移动时绝不会以引用的方式传递,除非你使用了std::ref()或者std::cref()包装的参数”。

因为没有使用std::ref()函数包装,所以std::bind()函数绑定的testObj对象实际上是原对象的副本,那么针对于副本的操作和修改自然就不会反应到原对象上,这也就是打印testObj.n_public会输出随机值的原因。

函数test3_2()在绑定时并没有具体到特定的对象,而是使用了placeholders::_1占位符,这样生成的函数,在调用的时候再传入操作对象,那么此时修改对象属性就可以起作用了。

函数test3_3()是针对于函数test3_1()的,添加了std::cref()包装的原对象,可以通过绑定后的函数修改。

6.总结

std::bind()函数可以嵌套绑定
std::bind()函数绑定成员函数时,函数名参数后面需要紧跟着类的对象作为参数
td::bind()不仅可以绑定普通函数、成员函数、还可以绑定成员变量

2.std::ref和std::cref

std::ref 用于包装按引用传递的值
std::cref 用于包装按const引用传递的值

1.为什么需要std::ref和std::cref

bind()是一个函数模板,它的原理是根据已有的模板,生成一个函数,但是由于bind()不知道生成的函数执行的时候,传递进来的参数是否还有效。所以它选择参数值传递而不是引用传递。如果想引用传递,std::ref和std::cref就派上用场了。

#include <functional>
#include <iostream>

void f(int& n1, int& n2, const int& n3)
{
    std::cout << "In function: n1[" << n1 << "]    n2[" << n2 << "]    n3[" << n3 << "]" << std::endl;
    ++n1; // 增加存储于函数对象的 n1 副本
    ++n2; // 增加 main() 的 n2
    //++n3; // 编译错误
    std::cout << "In function end: n1[" << n1 << "]     n2[" << n2 << "]     n3[" << n3 << "]" << std::endl;
}

int main()
{
    int n1 = 1, n2 = 1, n3 = 1;
    std::cout << "Before function: n1[" << n1 << "]     n2[" << n2 << "]     n3[" << n3 << "]" << std::endl;
    std::function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    //auto bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    //n1是值传递,函数内部的修改对外面没有影响。
    //n2是引用传递,函数内部的修改影响外面。
    //n3是const引用传递,函数内部不能修改。
    bound_f();
    std::cout << "After function: n1[" << n1 << "]     n2[" << n2 << "]     n3[" << n3 << "]" << std::endl;
}

1.2运行结果:

Before function: n1[1]     n2[1]     n3[1]
In function: n1[1]    n2[1]    n3[1]
In function end: n1[2]     n2[2]     n3[1]
After function: n1[1]     n2[2]     n3[1]

1.3结果分析

n1是值传递,函数内部的修改对外面没有影响。
n2是引用传递,函数内部的修改影响外面。
n3是const引用传递,函数内部不能修改。

3.std::ref()和&

最近看到一个多线程代码如下:

typedef unsigned long long ULL;

void accumulator_function(const std::vector<int> &v, ULL &acm,
    unsigned int beginIndex, unsigned int endIndex)
{
    acm = 0;
    for (unsigned int i = beginIndex; i < endIndex; ++i)
    {
        acm += v[i];
    }
}

int main()
{
    ULL acm1 = 0;
    ULL acm2 = 0;
    std::vector<int> v = { 1,2,3,4,5,6,7,8,9,10 };

    std::thread t1(accumulator_function, std::ref(v),
        std::ref(acm1), 0, v.size() / 2);

    std::thread t2(accumulator_function2, std::ref(v),
        std::ref(acm2), v.size() / 2, v.size());

    t1.join();
    t2.join();

    std::cout << acm1 << "+" << acm2 << "=" << acm1 + acm2 << std::endl;
    return 0;
}

其中创建线程的部分使用了std::thread t1(accumulator_function2, std::ref(v), ​ std::ref(acm1), 0, v.size() / 2);,对应的函数实现为void accumulator_function(const std::vector<int> &v, ULL &acm, ​ unsigned int beginIndex, unsigned int endIndex),该函数的参数为一个vector引用、一个计算结果acm的引用和记录vector首末位置的index。其中的std::ref()之前在C++ Primer中看过,感觉应该和&差不多吧,但是既然这样为什么仍然需要用ref转换成引用形式呢?立刻把std::ref()全部删了,重新运行,结果报错了…有点意思,可以探究一波。

1.探究过程

引用的例子首先列举一个:

void fun(vector<int> &a, int i)
{
    a[i] = 20;
}

int main()
{
    std::vector<int> v = { 1,1,1,1,1,1 };
    for (auto x : v)
        cout << x << ' ';
    cout << endl;
    fun(v, 3);
    for (auto x : v)
        cout << x << ' ';
}

// Output:
// 1 1 1 1 1 1
// 1 1 1 20 1 1

如果是普通引用的话,只需要调用fun(v, 3)就行了,为什么在例子中使用了fun(std::ref(v),…)这种形式呢?说明std::ref()和&是不一样的么?写个例子看一下:

#include <iostream>
#include <type_traits>
#include <functional>
using namespace std;
int main()
{
    int x = 5;
    //std::cout << boolalpha :将bool类型通过true或false的形式输出;
    std::cout << boolalpha << is_same<int&, decltype(ref(x))>::value;
    return 0;
}

输出答案果然是false!那么如果std::ref()返回的不是对象的引用,返回的是什么?查一下手册可以发现:函数模板 refcref是生成 std::reference_wrapper 类型对象的帮助函数,它们用模板实参推导确定结果的模板实参。所以std::ref()返回的实际上是一个reference_wrapper而不是T&,可以从一个指向不能拷贝的类型的对象的引用生成一个可拷贝的对象。 std::reference_wrapper的实例是对象(它们可被复制或存储于容器),但它们能隐式转换成 T& ,故能以之为以引用接收底层类型的函数的参数。
修改一下上面的例子,看看结果:

#include <iostream>
#include <type_traits>
#include <functional>
using namespace std;
int main()
{
    int x = 5;
    //is_same<U, T>::value;判断两个类型是否相等,相等返回True;
    std::cout << boolalpha << is_same<int&, decltype(ref(x).get())>::value;
    return 0;
}

Note:参考

变为true了。reference_wrapper与&并不一样,但是利用get()函数就是&类型。但是为什么在多线程那个例子中要使用std::ref()呢?

原因是,考虑了函数式编程(如std::bind)在使用时,是对参数直接拷贝,而不是引用。具体可以参照这一句话:std::reference_wrapper 用于按引用传递对象给 std::bind 或 std::thread 的构造函数;
还是通过代码理解一下:

#include <functional>
#include <iostream>
 
void f(int& n1, int& n2, const int& n3)
{
    std::cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    ++n1; // increments the copy of n1 stored in the function object
    ++n2; // increments the main()'s n2
}
 
int main()
{
    int n1 = 1, n2 = 2, n3 = 3;
    std::function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    n1 = 4;
    n2 = 5;
    n3 = 6;
    std::cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    bound_f();
    std::cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
}

得到的答案为:

Before function: 4 5 6
In function: 1 5 6
After function: 4 6 6

上述代码在执行std::bind后,在函数f()中n1的值仍然是1,n2和n3改成了修改的值。说明std::bind使用的是参数的拷贝而不是引用。C++11的设计者认为bind默认应该采用拷贝,如果使用者有需求,加上std::ref()即可。同理std::thread也是这样。

2.结论

std::ref只是尝试模拟引用传递,并不能真正变成引用,在非模板情况下,std::ref根本没法实现引用传递,只有模板自动推导类型时,ref能用包装类型reference_wrapper来代替原本会被识别的值类型,而reference_wrapper能隐式转换为被引用的值的引用类型,但是并不能被用作&类型。

Note:参考
std::reference_wrapper:

//定义于头文件 <functional>		
template< class T >
class reference_wrapper;
		//(C++11 起)

std::reference_wrapper 是包装引用于可复制、可赋值对象的类模板。它常用作将引用存储入无法正常保有引用的标准容器(类似 std::vector)的机制。

特别是, std::reference_wrapper 是围绕到类型 T 的对象引用或函数引用的可复制构造 (CopyConstructible) 且可复制赋值 (CopyAssignable) 的包装器。 std::reference_wrapper的实例是对象(它们可被复制或存储于容器),但它们能隐式转换成 T& ,故能以之为以引用接收底层类型的函数的参数。

若存储的引用可调用 (Callable) ,则可以相同参数调用 std::reference_wrapper

辅助函数std::refstd::cref常用于生成 std::reference_wrapper对象。

std::reference_wrapper 亦用于按引用传递对象给 std::bindstd::thread的构造函数。

保证 std::reference_wrapper为可平凡复制 (TriviallyCopyable) 。
(C++17 起)
T 可为不完整类型。
(C++20 起)

而回到刚开始的那个多线程代码,thread的方法传递引用的时候,我们希望使用的是参数的引用,而不是浅拷贝,所以必须用ref来进行引用传递。

作者:MrYun
链接:https://www.jianshu.com/p/277675675593
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值