共计 3866 个字符,预计需要花费 10 分钟才能阅读完成。
文章首发
【重学 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
对调用者是通明的。要达到这个目标有两个前提:
- 传给
factory_v1
的入参arg
可能完完整整 (包含援用属性、const 属性等) 得传给T
的构造函数。 - 工厂函数
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 call
auto 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 call
auto 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
,推导出模版函数foo
中ParamType
和 param
的类型。
类型推导的规定有很多,大家感兴趣能够去看看《Effective C++》[1],这里,咱们只介绍一种比拟非凡的万能援用。万能援用的模版函数格局如下:
template<typename T>
void foo(T&& param);
万能援用的
ParamType
是T&&
,既不能是const T&&
,也不能是std::vector<T>&&
万能援用的规定有三条:
- 如果
expr
是左值,T
和param
都会被推导成左值援用。 - 如果
expr
是右值,T
会被推导成对应的原始类型,param
会被推导成右值援用(留神,尽管被推导成右值援用,但因为param
有名字,所以自身还是个左值)。 - 在推导过程中,
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_v4
和std::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_v4
和std::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 ++ 学习交换群。不卖课,不免费,也禁止打广告,纯交流学习。(留神要备注,不然不给过了!)