简单介绍
C++引入右值引用,是为了支持“移动语义”和“完美转发”。不过在讨论它们之前,让我们先简单了解一下右值引用。
首先,什么是左值,什么是右值?
简单来说,左值是在内存中有具体表示,而且可以通过一个代表内存地址的变量名来引用的值;其他的都是右值。
// lvalues:
//
int i = 42;
i = 43; // ok, i is an lvalue
int* p = &i; // ok, i is an lvalue
// rvalues:
//
int foobar();
int* p2 = &foobar(); // error, cannot take the address of an rvalue
int j = foobar(); // ok, foobar() is an rvalue
int* p3 = &j; // ok, j is lvalue
j = 42; // ok, 42 is an rvalue
使用连续的两个&符号: && 表示右值引用,例如:void foo(std::string&& str)
。这个函数只接受右值:
foo(std::string("hello, world!")); // ok
std::string functionThatReturnString() {
return std::string("hello");
}
foo(functionThatReturnString()); // ok
std::string str;
foo(str); // error
如果还没开始头疼的话,下面这个很可能让你开始头疼:
右值引用不是右值。一旦一个右值引用绑定了一个右值,右值的这片临时内存就可以通过这个右值引用访问了,因此一个绑定了右值的右值引用是一个左值。
void bar(std::string &str);
void foo(std::string&& str) {
bar(str); // ok
bar(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 constructor
Foo 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>
### 非模板函数转发
让我们看看下面的例子:
```cpp
Foo createFooByBuilder(FooBuilder&& builder);
Foo createFoo(FooBuilder&& builder) {
std::cout << "create foo with builder" <<std::endl;
builder.setPropertyA("A");
createFooByBuilder(builder); // error, builder is now lvalue
createFooByBuilder(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)); // ok
acceptFoo(std::move(foo)); // ok, recommended
模板类型推导,引用折叠规则
模板类型推导时,存在引用折叠规则:
template <typename T> // T 是要推导的类型
void f(ParamType param) // ParamType 也是要推导的类型,是个和T有关的类型,
// 比如 void f(T& param),这里T& 就是 ParamType
f(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; // ok
using foo2 = Foo<int&>::foo2; // ok
using foo2 = Foo<int&>::foo; // error
using foo3 = Foo<int&&>::foo3; // ok
using 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!