![6d104414173887c8c52744d9c03b23d3.png](https://i-blog.csdnimg.cn/blog_migrate/5b8695f086368c84640d0fcabb7df931.jpeg)
Python的对象引用
众所周知,Python里的大部分对象都是存在堆上,变量名只是对堆上数据的引用,因此可以很轻易地对变量所指向的对象进行替换,如:
# 引用替换
同时,Python的参数传递也是传对象引用(pass-by-object-reference),也就是把对象的指针传过去,如:
# 传对象引用
以上两点在Python中让我们代码开发变得高效简洁,但是在C++中却没有这么好的特性。因此就引入C++ functional库中的reference_wrapper来让C++也能像Python这样简单地进行对象引用以及对象引用的替换。
在正式介绍reference_wrapper前, 先来看看C++中如何实现上述两个操作。
C++引用替换
如果我们把Python的引用替换代码直接翻译成C++,那显然是行不通的,如下:
auto
因为在C++中,对引用赋值与对原始对象赋值一样,并不会改变引用所指向对象的地址(不会把v指向的对象从object_A变成object_B),而只会改变所指向对象的值(如果能赋值成功的话,等同于object_A=object_B)。这是因为C++的引用只能被绑定一次,之后的赋值都是对对被绑定对象赋值(assign-through)。
如果想要在C++中实现引用替换的话,有多种变通方法。首先我们可以想到用lambda匿名函数来判断如何对这个引用变量赋值:
auto
这个方法虽然可以较为简单实现引用替换,但这不适用与逻辑相对复杂的情况,如先用指向object_A的v进行一系列操作,再把v指向另一个地方进行更多操作。而且也降低了代码的可读性。
当然我们也可以直接用原始的指针来实现:
auto
如果觉得原始指针在C++里不太雅观的话,也可以换成智能指针:
unique_ptr
但以上三种解决方法都无法像Python一样,用一个赋值号“=”轻松解决。因此,C++11开始,引入reference_wrapper[1]。上述过程可以写成:
reference_wrapper
这样,就可以像Python一样随意对引用的绑定对象进行替换,如果想要赋值的话,只需要加个get()即可:
v
C++传对象引用
那么我们再来看看如何传递对象引用给一个函数,这也是reference_wrapper提出的初衷。
同样的,我们把Python的传对象引用翻译成C++代码。首先,我们先定义一个函数,这个函数可以根据接受任意支持“++”操作的类型,对变量进行自增1:
template
C++想要让函数对一个对象进行操作,而不是对值操作后返回新的值,可以试用指针也可以试用引用,由于指针还需要进行解引用,也不符合Python代码风格,所以这里只考虑引用的情况。
此处如果我们不显示指定类型T,而让编译器去自动判断的话,如下例子会导致错误的结果:
int
这里并没有如我们预期的那样,输出自增后的结果11,而是原封不动输出10。这是因为编译器在对函数参数类型进行推断(argument deduction)的时候,不会自动推断引用类型,而是采用其原本的类型,除非我们显示指定它是个引用类型:
int
这样就会输出正确结果了。但是每次都要显示指定类型,就会使得代码变得很复杂。那么我们可以将其转化为reference_wrapper对象(ref和cref会返回reference_wrapper对象):
change
如此一来,我们就可以像Python一样,简洁地完成传对象引用的操作。
除此之外,reference_wrapper在传递构造函数参数(constructor argument forwarding)时更能凸显用处:
class
以上有两次引用操作,但因为参数推断的时候并不会显示向模板传递引用类型,导致Wrapper类初始化时错误。而reference_wrapper可以隐式转化为T&类型,能在不改变引用类型的情况下,进行模板类型推断。
reference_wrapper介绍
现在开始正式介绍reference_wrapper。
reference_wrapper的提出初衷是为了给引用提供一个可复制构造(copy constructible)和可赋值(assignable)的包装,它有个模板类reference_wrapper和两个返回reference_wrapper对象的函数ref和cref。旨在模板函数类型推断中,引用类型可以完整地被传递下去。
同时,它也希望达成一些普通引用无法做到的应用,如可以被存在容器中和可以用做函数引用并且可以直接被调用。举例如下:
// 存在容器中
reference_wrapper应用场景[2]
1、用于模板函数/构造函数,以避免显式指定类型/实例化临时对象
在C++11前声明模板函数类型的时候,如果想用到引用,就没有办法使用auto来自动类型推断,需要显示声明变量再初始化,而用了reference_wrapper则可以直接初始化让auto来做类型推断。
同时,如果初始化的引用对象是一个函数的返回值,通常情况下在初始化的时候该返回值的生命周期已经到了,没有办法进行引用,用reference_wrapper也可以规避这个问题。
举两例如下:
// 避免显式指定类型
2、用于容器存储
不像原始引用操作,reference_wrapper是一个对象,满足STL中容器对元素的要求(尤其是可擦除,erasable),因此reference_wrapper可以作为任意容器的元素类型。
vector
3、传对象引用
这点之前介绍过,它的优势在使用C++ thread的时候更为明显,因为thread的参数只能复制构造,如果需要thread对某个参数进行修改的话,即便用reference也会报错,如:
void
4、使含有引用成员的类可赋值/复制
假设一个函数使用一个引用变量初始化,并且将这个引用变量存在一个引用成员中,那么这个函数将无法被赋值,也无法进行复制,举例如下:
class
如果要解决这个问题,C++11之前只能避免使用引用作为类成员,而使用指针,但有了reference_wrapper即可避免这个问题:
class
5、传递可调用的函数对象引用
reference_wrapper可以像函数一样直接被调用,可以用来在STL算法中作为参数传递:
class
其他注意点
1、可以自动推断左值的const类型
const
2、只能与左值绑定
reference_wrapper
3、可以绑定不完整类
// 以下C++20开始可支持
class A;
void regular_reference(A& x);
void wrapper_reference(reference_wrapper<A> x);
4、可以对被绑定对象赋值[3]
这里举个稍微复杂的例子,假设我们要对链表进行初始化,每一行作为一个元素:
class
总结
reference_wrapper是C++11后一个很方便的特性,它可以使得引用能够重新绑定,同时又能显式指定是否对引用对象进行修改,还可以作为一个更强大的函数指针。实践过程中,它使得对于对象的直接操作变得简单很多,让C++开发过程中重复代码(尤其是类型推断)大大减少。
参考
- ^http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1436.html#class.std.reference_wrapper
- ^https://www.nextptr.com/tutorial/ta1441164581/stdref-and-stdreference_wrapper-common-use-cases
- ^https://www.youtube.com/watch?v=EKJMZCL00Ak&ab_channel=CppCon