1、介绍
右值引用是C++11的新特性,初次接触觉得很难理解。因此我们将不直接给出右值引用的定义,而是从没有引入右值引用之前存在的问题,引入右值引用之后问题是如何解决的。
在 C语言 最原始的定义中,左值可以位于赋值运算符左边或者右边;右值只能位于赋值运算符右边。
int a = 42;
int b = 43;
// a, b 都是左值
a = b; // ok
b = a; // ok
a = a * b; // ok
// a * b 是右值:
int c = a * b; // ok, 右值位于赋值运算符的右边,正确的
a * b = 42; // error, 右值位于赋值运算符的左边,错误的
但是在 C++ 中,由于用户自定义的类型,使得这里左值、右值的定义存在一些问题。左值表示存储在计算机内存的对象,可以被取地址;右值相当于内存地址中的数据,不能被取地址。看下面的代码实例:
int i = 42;
i = 43; // ok, 左值位于赋值运算符的左边,正确的
int* p = &i; // ok, 左值可以被取地址
int& foo();
foo() = 42; // ok, foo() 是一个左值
int* p1 = &foo(); // ok
// --------------------------------------------
int foobar();
int j = 0; //j 是左值
j = foobar(); // ok, 这里foobar() 是一个 ** 右值 **
int* p2 = &foobar(); // error, 右值不能被取地址
j = 42; // ok,
2、移动语言 move
假定一个类 X , 包含一个指针(设为m_pResource) 指向该类的资源,分析以下情况:
- 赋值运算符
X A;
X& X::operator=(X const & rhs) //operator= for left value
{
// [...]
}
这个赋值运算符包含以下步骤:
-
复制参数
rhs
对应的资源rhs.m_pResource
; -
释放
this->m_pResource
的资源; -
将复制的资源 复制 给
this->m_pResource
。 -
拷贝构造函数
X foo();
X x;
x = foo();
最后一行代码 x = foo();
包含以下步骤:
- 复制 函数
foo()
返回的临时对象(tmp)的资源,记为tmp.m_pResource
; - 释放
x.m_pResource
的资源,并将tmp.m_pResource
的资源拷贝给x.m_pResource
; - 析构临时对象tmp,释放资源 tmp.m_pResource。
显然,如果可以直接交换tmp
对象的资源 和x
对象的资源效率更高,然后调用tmp
的析构函数释放x
对象原来有用的资源,也就是移动语义。 可以通过重载operator=
来实现以上移动语义。定义中的<mystery type>
类型至关重要,从前面的要求可以看出,我们希望前面的等号右边的值foo()
可以通过引用来传递,进而实现交换资源。同时,我们还希望,对了重载了operator=
,左值仍然调用原来的operator=
函数,而右值调用这里重载的operator=
。
X& X::operator=(<mystery type> rhs) //operator= special for right value
{
// [...]
// 交换 this->m_pResource 和 rhs.m_pResource
// [...]
}
3、右值引用
如果 X
代表任意类型, X&&
表示 X
的右值引用,为了更好的区分X&&
和 X&
, 称X&
为左值引用。大部分情况下,右值引用表现的和左值引用的区别不大,但是在函数重载的时候,左值引用调用原来默认的函数,而右值引用调用上面声明为 <mystery type>
的函数。
X&&
------------右值引用
X&
--------------左值引用
void foo(X& x); // 左值引用重载
void foo(X&& x); // 右值引用重载
X x;
X foobar();
foo(x); // 参数 x 是左值,调用 foo(X& x)
foo(foobar()); // 参数是右值, 调用 foo(X&&)
右值引用的出现,使得编译器在编译期间根据参数的不同(左值引用还是右值引用)调用不同类型的 函数。虽然上面类型的重载函数可以被用于任何寒暑表,但是这种类型的重载还是主要被应用于 拷贝构造函数和赋值运算符的重载, 以实现移动语义。
X& X::operator=(X const & rhs); // classical implementation
X& X::operator=(X&& rhs)
{
// 移动语义,交换`this` 和 参数 `rhs` 的资源
return *this;
}
不能在使用为右值引用为参数的函数中使用左值引用作为参数,会导致编译错误。
4、强迫移动语义 swap
标准库函数 swap() 就是使用了移动语义。下面的代码中没有使用右值引用:
template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
}
X a, b;
swap(a, b);
但是我们知道在以下情况:“当一个变量作为拷贝构造函数或复制的参数的时候,该变量只是单纯的作为赋值的目标 ” 使用移动语义再合适不过了。
在 C++11 中,库函数 move() 将其参数变为右值引用,因此在C++11 中,swap() 函数实际上使用移动语义,注:下面的代码中 X
重载了右值引用 拷贝构造函数 和 赋值元素符。
template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
X a, b;
swap(a, b);
5、右值引用就是右值?
现在考虑下面一种情况(类X
重载了右值引用 拷贝构造函数 和 赋值元素符):
void foo(X&& x)
{
X anotherX = x;
// ...
}
思考上面函数foo
调用的是哪个版本的拷贝构造函数?
这里 x
被声明为右值引用,但是在上面的应用场景中,被声明为右值引用的变量有名字 x
,因此它是左值。
被声明为右值引用的参数可以被作为左值或者右值,区别是如果有名字就是左值;如果没有名字就是右值。
因此函数foo()
内部调用的是X(X const & rhs)
void foo(X&& x)
{
X anotherX = x; // 调用 X(X const & rhs),因为x 有名字
}
X&& goo();
X x = goo(); // 调用 X(X&& rhs) ,因为 goo() 返回的是没有名字的右值引用
没有名字就是右值,有名字就是左值。 swap() 函数将其参数变为右值引用,哪怕传递给它的参数不是右值引用。下面的例子展示了“如果有名字”这个前提的重要性:
Base(Base const & rhs); // 非移动语义
Base(Base&& rhs); // 移动语义
现在有一个子类派生自基类Base,为了确保移动语义被用于 派生类对象的基类部分,我们需要重载派生类的拷贝构造函数 和 赋值运算符。以拷贝构造函数为例分析,左值引用的拷贝构造函数的版本如下所示:
Derived(Derived const & rhs)
: Base(rhs)
{
}
但是,右值引用的拷贝构造函数并不仅仅只是将参数变为右值引用这么简单,下面的版本是错误的,会调用非移动拷贝构造函数,因为rhs
有名字,所以它是左值。
Derived(Derived&& rhs)
: Base(rhs) // 错误的,rhs 是一个左值
{
}
如果想正确调用右值引用拷贝构造函数,应该用下面的代码:
Derived(Derived&& rhs)
: Base(std::move(rhs)) // 调用 Base(Base&& rhs)
{
// Derived-specific stuff
}
6、移动语义和编译器优化
考虑下面的代码,(类X
重载了右值引用 拷贝构造函数 和 赋值元素符):
X foo()
{
X x;
//
return x;
}
如果你认为上面的代码在函数返回值和 x
之间存在值拷贝,然后你将代码修改为下面的形式,不幸的是,下面的代码是错误的,其实编译器处理下面这样的代码,会采取一定的优化,编译器直接在函数返回值处构造X
对象,因此不需要多此一举采用移动语义! 因此如果需要使用右值引用和引动语义,你需要了解编译器的一些优化,例如RVO 和 copy elision 。
X foo()
{
X x;
//
return std::move(x); // 大错特错!!!
}
分类 C++