文章首发

【重学C++】05 | 说透右值援用、挪动语义、完满转发(下)

引言

大家好,我是只讲技术干货的会玩code,明天是【重学C++】的第五讲,在第四讲《【重学C++】04 | 说透右值援用、挪动语义、完满转发(上)》中,咱们解释了右值和右值援用的相干概念,并介绍了C++的挪动语义以及如何通过右值援用实现挪动语义。明天,咱们聊聊右值援用的另一大作用 -- 完满转发

什么是完满转发

假如咱们要写一个工厂函数,该工厂函数负责创立一个对象,并返回该对象的智能指针。

template<typename T, typename Arg> std::shared_ptr<T> factory_v1(Arg arg){    return std::shared_ptr<T>(new T(arg));}class X1 {public:    int* i_p;    X(int a) {        i_p = new int(a);    }}

对于类X的调用方来说,auto x1_ptr = factory_v1<X1>(5); 应该与auto x1_ptr = std::shared_ptr<X>(new X1(5))是齐全一样的。

也就是说,工厂函数factory_v1对调用者是通明的。要达到这个目标有两个前提:

  1. 传给factory_v1的入参arg可能完完整整(包含援用属性、const属性等)得传给T的构造函数。
  2. 工厂函数factory_v1没有额定的副作用。

这个就是C++的完满转发。

单看factory_v1利用到X1貌似很"完满",但既然是工厂函数,就不能只满足于一品种对象的利用。假如咱们有类X2。定义如下

class X2 {public:    X2(){}    X2(X2& rhs) {        std::cout << "copy constructor call" << std::endl;    }}

当初大家再思考上面代码:

X2 x2 = X2();auto x2_ptr1 = factory_v1<X2>(x2);// output:// copy constructor call// copy constructor callauto x2_ptr2 = std::shared_ptr<X2>(x2)// output:// copy constructor call

能够发现,auto x2_ptr1 = factory_v1<X2>(x2);auto x2_ptr2 = std::shared_ptr<X2>(x2)多了一次拷贝构造函数的调用。

为什么呢?很简略,因为factory_v1的入参是值传递,所以x2在传入factory_v1时,会调用一次拷贝构造函数,创立arg。很间接的方法,把factory_v1的入参改成援用传递就好了,失去factory_v2

template<typename T, typename Arg> std::shared_ptr<T> factory_v2(Arg& arg){    return std::shared_ptr<T>(new T(arg));}

改成援用传递后,auto x1_ptr = factory_v2<X1>(5);又会报错了。因为factory_v2须要传入一个左值,但字面量5是一个右值。

办法总比艰难多,咱们晓得,C++的const X& 类型参数,既能接管左值,又能接管右值,所以,稍加革新,失去factory_v3

template<typename T, typename Arg> std::shared_ptr<T> factory_v3(const Arg& arg){    return std::shared_ptr<T>(new T(arg));}

factory_v3还是不够"完满", 再看看另外一个类X3

class X3 {public:    X3(){}    X3(X3& rhs) {        std::cout << "copy constructor call" << std::endl;    }    X3(X3&& rhs) {        std::cout << "move constructor call" << std::endl;    }}

再看看以下应用例子

auto x3_ptr1 = factory_v3<X3>(X3());// output// copy constructor callauto x3_ptr2 = std::shared_ptr<X3>(new X3(X3()));// output// move constructor call

通过上一节咱们晓得,有名字的都是左值,所以factory_v3永远无奈调用到T的挪动构造函数。所以,factory_v3还是不满足完满转发。

非凡的类型推导 - 万能援用

给出完满转发的解决方案前,咱们先来理解下C++中一种比拟非凡的模版类型推导规定 - 万能援用。

// 模版函数签名template <typename T>void foo(ParamType param);// 利用foo(expr);

模版类型推导是指依据调用时传入的expr,推导出模版函数fooParamTypeparam的类型。

类型推导的规定有很多,大家感兴趣能够去看看《Effective C++》[1],这里,咱们只介绍一种比拟非凡的万能援用。 万能援用的模版函数格局如下:

template<typename T>void foo(T&& param);
万能援用的ParamTypeT&&,既不能是const T&&,也不能是std::vector<T>&&

万能援用的规定有三条:

  1. 如果expr是左值,Tparam都会被推导成左值援用。
  2. 如果expr是右值,T会被推导成对应的原始类型,param会被推导成右值援用(留神,尽管被推导成右值援用,但因为param有名字,所以自身还是个左值)。
  3. 在推导过程中,expr的const属性会被保留下来。

看上面示例

template<typename T>void foo(T&& param);// x是一个左值int x=27;// cx是带有const的左值const int cx = x;// rx是一个左值援用const int& rx = cx;// x是左值,所以T是int&,param类型也是int&foo(x);// cx是左值,所以T是const int&,param类型也是const int&foo(cx);// rx是左值,所以T是const int&,param类型也是const int&foo(rx);// 27是右值,所以T是int,param类型就是int&&foo(27);

std::forward实现完满转发

到此,完满转发的前置常识就曾经讲完了,咱们看看C++是如何利用std::forward实现完满转发的。

template<typename T, typename Arg> std::shared_ptr<T> factory_v4(Arg&& arg){   return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));}

std::forward的定义如下

template<class S>S&& forward(typename remove_reference<S>::type& a) noexcept{  return static_cast<S&&>(a);}

传入左值

X x;auto a = factory_v4<A>(x);

依据万能援用的推导规定,factory_v4中的Arg会被推导成X&。这个时候factory_v4std::forwrd等价于:

shared_ptr<A> factory_v4(X& arg){   return shared_ptr<A>(new A(std::forward<X&>(arg)));}X& std::forward(X& a) {  return static_cast<X&>(a);}

这个时候传给A的参数类型是X&,即调用的是拷贝构造函数A(X&)。合乎预期。

传入右值

X createX();auto a = factory_v4<A>(createX());

依据万能援用推导规定,factory_v4中的Arg会被推导成X。这个时候factory_v4std::forwrd等价于:

shared_ptr<A> factory_v4(X&& arg){   return shared_ptr<A>(new A(std::forward<X>(arg)));}X&& forward(X& a) noexcept{  return static_cast<X&&>(a);}

此时,std::forward作用与std::move一样,暗藏掉了arg的名字,返回对应的右值援用。这个时候传给A的参数类型是X&&,即调用的是挪动构造函数A(X&&),合乎预期。

总结

这篇文章,咱们次要是持续第四讲的内容,一步步学习了完满转发的概念以及如何应用右值解决参数透传的问题,实现完满转发。

[1] https://github.com/CnTransGroup/EffectiveModernCppChinese/blob/master/src/1.DeducingTypes/item1.md
<center> END </center>

【往期举荐】

【重学C++】01| C++ 如何进行内存资源管理?

【重学C++】02 | 脱离指针陷阱:深入浅出 C++ 智能指针

【重学C++】03 | 手撸C++智能指针实战教程

【重学C++】04 | 说透C++右值援用、挪动语义、完满转发(上)

【学习交换】

可增加微信huiwancoder,备注cpp,拉你进c++学习交换群。不卖课,不免费,也禁止打广告,纯交流学习。(留神要备注,不然不给过了!)