0、完美转发(定义)
所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
1、完美转发(存在的问题)
右值引用还可以被用来解决完美转发的问题,考虑下面的代码,很明显代码的意图是将Arg
参数通过 factory
函数转发到 T 的构造函数中。理想情况下,就参数Arg
而言,就像factory
函数不存在,直接调用客户代码的构造函数一样,即完美转发。不幸的是,代码在这种状况下会失败:拷贝构造函数中采取值传递的方式,会导致无限循环。
template<typename T, typename Arg>
shared_ptr<T> factory(Arg arg)
{
return shared_ptr<T>(new T(arg));
}
通常情况下,我们采用 boost::bind
来解决上面的问题。
template<typename T, typename Arg>
shared_ptr<T> factory(Arg& arg)
{
return shared_ptr<T>(new T(arg));
}
但是上面的解决办法也不是完美的,现在的问题是 factory
函数不能传入右值作为参数。
factory<X>(hoo()); //如果函数 hoo() 以值传递的方式返回
factory<X>(41); // 错误
常量引用const &
定义表面上可以 解决上面的问题
template<typename T, typename Arg>
shared_ptr<T> factory(Arg const & arg)
{
return shared_ptr<T>(new T(arg));
}
但是使用常引用存在两个问题:首先,如果factory
不止一个参数,有多个参数,需要提供所有相应的重载拷贝构造函数,可扩展性差;其次,这种情形下的转发不能成为完美转发,因为并没有利用移动语义,因为T
中的参数是以值传递的方式。
以上的两个问题可以被右值引用解决,以实现真正意义上的完美转发,在介绍右值引用如何解决这个问题前,需要先介绍下面的内容。
2、完美转发(解决办法)
首先介绍C++11 中的新特性:允许对引用取引用。在之前的版本中,A& &
会编译出错,但是在 C++11 中,引用重叠解释为下面的含义。
A& & becomes A&
A& && becomes A&
A&& & becomes A&
A&& && becomes A&&
其次,C++ 11 对于函数模板,有一个特别参数规则:当其参数声明为右值引用时,(1)当传入左值的时候,T
调用的是 A&
构造函数,因为 上面的引用重叠规则;(2)当传入右值的时候,T
调用的是 A&&
构造函数。
template<typename T>
void foo(T&&);
有了上面的新特性之后,
template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
//其中,forward 定义如下
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
//不用关心关键字 noexcept,这个关键字是为了告诉编译器这个函数不抛出异常
{
return static_cast<S&&>(a);
}
结合引用重叠规则,分析, 以 X
类型 为例:
- 如果传入的参数是
X
左值类型的,则arg
被推导为X&
,然后传递给std::forward
的模板参数,X& &&
被解释为X &
,实现了完美转发;
X x;
factory<A>(x);
shared_ptr<A> factory(X& && arg)
{
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& && forward(remove_reference<X&>::type& arg) noexcept
{
return static_cast<X& &&>(arg);
}
利用引用重叠规则,可解释为:
shared_ptr<A> factory(X& arg)
{
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& std::forward(X& a)
{
return static_cast<X&>(a);
}
- 如果传入的参数是
X
右值类型的,则arg
被推导为X&&
,然后传递给std::forward
的模板参数,X&& &&
被解释为X &&
,实现了完美转发;
//X fun();
factory<A>(fun());
shared_ptr<A> factory(X&& && arg)
{
return shared_ptr<A>(new A(std::forward<X&&>(arg)));
}
X&& && forward(remove_reference<X&&>::type& arg) noexcept
{
return static_cast<X&& &&>(arg);
}
利用重叠引用规则,简化为:
factory<A>(fun());
shared_ptr<A> factory(X && arg)
{
return shared_ptr<A>(new A(std::forward<X&&>(arg)));
}
X && forward(remove_reference<X&&>::type& arg) noexcept
{
return static_cast<X &&>(arg);
}
- 如果传入的参数是
X &
类型的,则arg
被推导为X &
,然后传递给std::forward
的模板参数,X& &&
被解释为X &
,实现了完美转发;
shared_ptr<A> factory(X & && arg)
{
return shared_ptr<A>(new T(std::forward<X &>(arg)));
}
X & && forward(typename remove_reference<X &>::type& a) noexcept
{
return static_cast<X & &&>(a);
}
利用引用重叠规则,可解释为:
shared_ptr<A> factory(X& arg)
{
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& std::forward(X& a)
{
return static_cast<X&>(a);
}
-
如果传入的参数是
const X &
类型的,则arg
被推导为const X &
,然后传递给std::forward
的模板参数,const X& &&
被解释为const X &
,实现了完美转发; -
如果传入的参数是
const X
类型的,则arg
被推导为const X &
,然后传递给std::forward
的模板参数,const X& &&
被解释为const X &
,实现了完美转发; -
move() 代码
template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
假定我们调用move() , 传入左值作为参数,则代码会被解释为
X x;
std::move(x);
typename remove_reference<X&>::type&&
std::move(X& && a) noexcept
{
typedef typename remove_reference<X&>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
简化为
X&& std::move(X& a) noexcept
{
return static_cast<X&&>(a);
}
从上面的代码可以看出,move() 函数中,当传入一个左值x
的时候,左值将绑定为右值引用,然后将其转化为右值引用。
2、右值引用与异常
如果在代码中使用右值引用,应该注意下面两个方面:
- 确保重载的右值引用拷贝构造函数或者赋值运算符 编写正确,这个很简单,因为移动语义通常就是交换两个对象的指针和它们控制的资源而已,确保这部分不出错即可;
- 使用关键字
noexcept
来告诉编译器在重载的右值引用拷贝构造函数或者赋值运算符中不要抛出异常;
正确使用右值引用需要时刻记住下面三点:
- 重载函数,像下面的定义一样,这样可以在编译期间,确定调用的是哪个函数。当然其中比较关键的是当使用移动语义的时候对于 拷贝构造函数和赋值运算符的重载。编写重载函数的时候一定要注意异常的处理,确保程序编写正确的前提下,尽量使用关键字
noexcept
。
void foo(X& x); // 左值引用重载
void foo(X&& x); // 右值引用重载
-
std::move() 将传入的参数转化为 右值
-
std::forward() 可以实现真正的完美转发。
分类 C++