从示例到源码探究std::ref


概述

相信大部分人在使用的时候是从多线程那了解的,在启动一个线程的时候如果要执行的函数是一个引用,这时候就需要我们使用std::ref进行包裹,那么究竟是怎么起作用的呢。先看一下官方手册里面的定义是什么。

例子

自定义值传递函数模板

我们先引入代码,观察并思考如下代码

 template<typename Fn,typename ...Args>
 auto call_by_value(Fn &&fn,Args... args)
 {
     return fn(args...);
 }
 void func(int &a)
 {
     a = 1;
 }
 int main()
 {
     int a = 0;
     call_by_value(func,a);
     std::cout << a << std::endl;
 }

由于模板函数call_by_value是以值传递的方式接受参数的,所以该模板函数在调用的时候,首先会对参数进行一次值拷贝,构造一个临时的实参对象,然后将实参对象传递给func函数。

func函数开始执行的时候,函数形参接受的是外部传入变量的引用,然后在函数内部将这个引用对应的变量赋值为1。由于这里面在外部函数call_by_value函数调用中,参数是以值传递的方式进行实现的。所以func函数内变量a这个引用指向的是call_by_value的函数实参,即临时构造的变量

所以,经过函数调用,变量并不会被修改。值还是0。所以上述代码运行之后的终端输出如下

 0

在实际的编程实践中,我们使用其他模板函数的时候,有时会遇到该模板函数是通过值的方式对参数进行接受的。但是我们希望希望该模板函数接受的是引用。比如上边的代码中,我们的func代码接受的是引用,目的是在函数内部对变量进行计算更改。比如在使用std::bind进行柯里化函数的时候,待柯里化的函数形参是引用,这个时候就无法进行形参的有效传递。

bind工具函数模板

我们在实际的编程实践中,经常需要通过bind进行函数柯里化,比如std::find_if的谓词函数仅仅接受一个参数的函数,如果你的比较函数是二元谓词函数,则需要进行柯里化,示例如下

 bool great_than(int first,int second)
 {
   return first > second;
 }
 std::vector<int> data = {1,2,3,4,5};
 //找到第一个大于3的元素
 auto it = std::find_if(std::cbegin(data),std::cend(data),std::bind(great_than,std::placeholders::_1,3));

考虑如下代码,实现了一个函数func在函数函数的形参是引用类型的,函数的目的是对传递进来的变量进行值的修改,修改成1:

 void func(int &a)
 {
     a = 1;
 }

 int main()
 {
     int a = 0;
     auto wrap_func = std::bind(func,a);
     std::cout << a << std::endl;
     return 0;
 }

最后答案与之前一样,都是0,原因也是因为在函数包装的过程中,将引用类型的参数进行了值的传递导致的,但是bind模板函数略有不同。

直接进去看函数定义:

这个函数模板的签名中用来传递参数的__bound_args是一个转发引用(forwarding reference,在以前的叫法为万能引用,但这种叫法已经过时,在现有的C++标准中叫转发引用)。当我们传递一个变量名的标识符进来的时候,该参数是以引用的形式传递的。没错,在这一层的函数调用中当传递一个函数名的时候,变量是通过左值引用的方式进行传递。

但是,由于柯里化的需求上面的bind函数需要做的是,将可调用对象__f和参数进行一个收集,构建并且返回另一个可调用对象给调用者,由调用者对这个调用对象进行调用并且传入剩余参数(也可以在调用bind函数进行柯里化的时候一次性将所有参数传递进来,这样在调用得到的可调用对象的时候就不需要传递剩余参数了,但是这样就失去了柯里化的意义)。

问题关键就是这个可调用对象的构建以及参数收集上,也就是这里面的__bind的对象的构建,下面我们看一下这个对象的构造函数相关代码:

注意两个成员变量

构造函数

该函数内部通过__bound_args_成员变量对函数参数的值进行存储,并且是值的方式通过一个元组tuple进行存储的。这会导致当我们使用bind对一个具有引用类型形参的函数进行柯里化的时候,传入的引用类型的形参将会以值传递的形式进行参数收集保存。这与我们想要传递变量的引用,并且在函数内部对该变量进行值的修改的需求不符合。

std::ref

当我们需要使用可能上面两种进行值传递的方式进行函数参数转发的进行带引用函数形参的的时候,就不能对直接传递原始的数据了。为了解决这个问题,C++标准库提供了一个帮助函数模板std::ref,相当于打了个补丁。该帮助函数模板通过左值引用类型的形参接受变量,并且将该变量包装成一个另外的类型返回。

函数内部构造了一个reference_wrapper类型对象并且将变量的左值引用传入到该类型的构造函数,让该对象持有这个左值引用。

  • 首先是指针引用部分_TP* _M_data,用来持有这个被修饰对象的相关信息的。
  • 构造函数通过addressof(__f)函数取出该变量的地址,将这个地址信息存入成员变量中

为了保证引用类型在经过函数模板或者类模板中的值传递过程中可以保持引用信息。这里面采用将传入变量包装成另外一个新的对象,在这个新的对象中持有被包装对象的地址信息。

在函数模板和类模板的值传递过程中,对这个新的对象进行值传递,其内部的被包装的对象地址信息可以得到保存。在函数模板或者内模板内部使用这个新的对象的时候,可以通过重载的类型转换函数将被包装变量的地址信息转换还原成相应的引用,对这个引用进行操作。从而达到操作外部变量的作用。

addressof这个函数用来取对象的地址。如果一个类型没有重载取地址操作符&,则使用该函数和使用取地址操作符效果是一致的。如果这个类型重载了取地址操作符(这种情况不常见,但是也有),则想取得对象的内存地址则无法通过取地址操作符,只能通过这个函数。函数库里面使用这个函数是为了可以兼容所有的情况。我们在自己的代码中如果是确认取实际的内存地址信息,也因该使用addressof

通过std::ref修改示例代码

下面我们通过std::ref对上面的代码进行修改,首先是自定义值传递函数模板示例代码的修改:

 template<typename Fn,typename ...Args>
 auto call_by_value(Fn &&fn,Args... args)
 {
     return fn(args...);
 }
 void func(int &a)
 {
     a = 1;
 }
 int main()
 {
     int a = 0;
     call_by_value(func,std::ref(a));
     std::cout << a << std::endl;
 }

上述代码中将第13行中的第2个参数a通过std::ref进行包装传入,代码运行结果如下

1

另一个示例代码修改同理,在使用上可以说是非常方便。


文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录