简单介绍
C++引入右值引用,是为了支持“移动语义”和“完美转发”。不过在讨论它们之前,让我们先简单了解一下右值引用。
首先,什么是左值,什么是右值?
简单来说,左值是在内存中有具体表示,而且可以通过一个代表内存地址的变量名来引用的值;其他的都是右值。
// lvalues://int i = 42;i = 43; // ok, i is an lvalueint* p = &i; // ok, i is an lvalue// rvalues://int foobar();int* p2 = &foobar(); // error, cannot take the address of an rvalueint j = foobar(); // ok, foobar() is an rvalueint* p3 = &j; // ok, j is lvaluej = 42; // ok, 42 is an rvalue
使用连续的两个&符号: && 表示右值引用,例如:void foo(std::string&& str)。这个函数只接受右值:
foo(std::string("hello, world!")); // okstd::string functionThatReturnString() {return std::string("hello");}foo(functionThatReturnString()); // okstd::string str;foo(str); // error
如果还没开始头疼的话,下面这个很可能让你开始头疼:
右值引用不是右值。一旦一个右值引用绑定了一个右值,右值的这片临时内存就可以通过这个右值引用访问了,因此一个绑定了右值的右值引用是一个左值。
void bar(std::string &str);void foo(std::string&& str) {bar(str); // okbar(std::string("hello")); // error}foo(std::string("helloworld"));
记住了上面的概念后,让我们用右值引用来解决移动语义和完美转发问题,来消化消化这些概念。
移动语义
右值具备一个相当有用的特性:它们是在当前语句之后,就会被销毁的值,它们会临时占据一片内存,但由于这片内存在当前语句之后就不可访问了,因此编译器可以立即安全的销毁这片内存。
因为右值的这种特性,我们可以安全的将右值对象的内容“移动”走,将右值对象内容“移动”到我们的左值对象中。
在定义类时,我们需要明确我们的类是否支持move constructor以及move assignment operator,它们的定义方式如下(实现可以参考RAII那篇文章):
class Foo {public:// rhs引用的是一个在当前函数调用结束时,就会被销毁的对象,因此我们可将rhs的内容“移动”到this中,注意这里要做的是“移动”,而不是“赋值”,移动之后,右值对象被移动的属性必须要被清空Foo(Foo&& rhs) {...}// 同上,rhs即将被销毁,但这里做的是赋值,相比上面的构造,赋值后我们要确保this原来管理的// 资源被销毁,这可以通过“交换”this和rhs资源来实现,利用rhs即将被销毁的特点,将希望被销// 毁的this管理的资源和rhs管理的资源互换,这样rhs销毁时,就会销毁原来由this管理的资源。Foo& operator=(Foo&& rhs) {...}}// now we can do:Foo createFoo();Foo(createFoo()); // move constructorFoo foo;foo = createFoo(); // move assignment
右值引用让下面两种“移动”操作成为可能:
如果一个变量绑定的值马上就要被销毁了,而我们又需要这个变量绑定的值,我们就可以将值的某些内容安全地“移动”到另一个变量值里。
std::string hello = "hello";std::string world = "world";// 没有右值构造函数,string只能拷贝一份 hello + world 产生的临时string对象的数据std::string helloWorld = hello + world;
让我们能够使用C++定义可移动但不可拷贝的类,例如:unique_ptr、unique_lock ```cpp std::mutex gMutex;
std::unique_lock
void startProcessA() { auto lock = innerStartLocked(); // process A }
void startProcessB() { auto lock = innerStartLocked(); // process B }
<a name="IORil"></a>## 完美转发有了右值语义后,还有一个问题,就是右值一旦绑定到变量上,这个变量就是左值了。那么,如果一个函数接收一个右值作为参数,然后又想传递这个参数给另一个接收右值的函数,该怎么办?<a name="N1F6C"></a>### 非模板函数转发让我们看看下面的例子:```cppFoo createFooByBuilder(FooBuilder&& builder);Foo createFoo(FooBuilder&& builder) {std::cout << "create foo with builder" <<std::endl;builder.setPropertyA("A");createFooByBuilder(builder); // error, builder is now lvaluecreateFooByBuilder(std::move(builder)); // ok}
createFoo接受一个右值,但是一旦右值绑定到右值引用变量,这个右值引用就成为一个左值,后续调用另一个接收右值的函数时,就会报错。这种场景下,需要使用std::move将左值转为右值。
不要乱用std::move,一定要在清楚知道被move后的变量不会再被使用的前提下,再使用std::move。这种绑定了一个即将销毁的右值的变量,在C++标准中有个专门的名字:xvalue,x 表示混合,因为这种变量虽然是lvalue,但绑定的值我们清楚的知道是rvalue。
模板函数转发(完美转发)
再让我们看看模板函数的情况,假如我们想提供一个如下创建shared_ptr的模板函数,在参数是左值时,调用拷贝构造函数,在参数是右值时,调用移动构造函数,我们应该这样做:
template<typename T, typename Arg>shared_ptr<T> factory(const Arg&& arg) {return shared_ptr<T>(new T(std::forward<Arg>(arg)));}
上面,std::forward函数的作用是,如果factory被调用时传入的arg是左值,将arg转为左值引用;如果传入的arg是右值,将arg转为右值引用。这种效果,就是完美转发。
深入理解std::move和std::forward
前面介绍了std::move和std::forward的作用,下面介绍它们的实现。但首先,我们需要了解一些前置知识
左值可以强转为右值
可以使用static_cast将左值强转为右值,std::move封装了这一操作:
void acceptFoo(Foo&& foo);Foo foo;acceptFoo(static_cast<Foo&&>(foo)); // okacceptFoo(std::move(foo)); // ok, recommended
模板类型推导,引用折叠规则
模板类型推导时,存在引用折叠规则:
template <typename T> // T 是要推导的类型void f(ParamType param) // ParamType 也是要推导的类型,是个和T有关的类型,// 比如 void f(T& param),这里T& 就是 ParamTypef(expr) // 调用模板的代码,编译器要为这个调用推导合适的类型
- 如果ParamType写成 T&& ,而param是:
- 左值,则ParamType会被推导为 T&
- 右值,则ParamType会被推导为 T&&
- 如果ParamType写成 T&,则不论param是左值还是右值,ParamType都会被推导为 T&
- 如果ParamType写成 T,则不论param是左值还是右值,ParamType都会被推导为T
模板类的偏特化
可以对一个模板类进行偏特化,在选择合适的模板时,编译器会选择和提供的模板类型最匹配的一个。
template<typename T> struct Foo {using foo = T;}; // 模板类定义template<typename T> struct Foo<T&> {using foo2 = T;}; // 模板类的偏特化template<typename T> struct Foo<T&&> {using foo3 = T;}; // 模板类的偏特化using foo = Foo<int>::foo; // okusing foo2 = Foo<int&>::foo2; // okusing foo2 = Foo<int&>::foo; // errorusing foo3 = Foo<int&&>::foo3; // okusing foo3 = Foo<int&&>::foo2; // error
模板元编程技巧,remove_reference_t
利用上面的规则:
如果ParamType写成 T,则不论param是左值还是右值,ParamType都会被推导为T
可以编写一个用于萃取无引用类型的模板:
template< typename T > struct remove_reference { typedef T type; };template< typename T > struct remove_reference<T&> { typedef T type; };template< typename T > struct remove_reference<T&&> { typedef T type; };template<typename T>using remove_reference_t = typename remove_reference<T>::type;
std::move的实现
template<typename T>remove_reference<T>::type&& move(T&& arg){return static_cast<remove_reference_t<T>&&>(arg);}
std::forward的实现
template<class S>S&& forward(typename remove_reference<S>::type& a) noexcept{return static_cast<S&&>(a);}
其他
const reference
C++11之前,const reference也可以接收右值,但是const reference同样可以接收const左值,这是它和右值引用的差异。
返回值优化 RVO
有些看起来会调用move constructor的代码,由于编译器普遍会进行RVO,会避免move constructor调用,不过,这种优化是在不影响代码语义的前提下进行的,如果返回的类对象不支持move constructor,编译器会在编译时就报错。
Foo createFoo() {Foo foo();foo.setA("a");return foo; // 如果foo没有move constructor,clang会报错}auto foo = createFoo(); // no move constructor called!
