C++11新特性之0——移动语义、移动构造函数和右值引用

时间:2023-12-19 09:47:56

C++引用现在分为左值引用(能取得其地址)和 右值引用(不能取得其地址)。其实很好理解,左值引用中的左值一般指的是出现在等号左边的值(带名称的变量,带*号的指针等一类的数据),程序能对这样的左值进行引用获得其地址;右值引用中的右值一般指的就是出现在等号右边的值(右值引用:常量、表达式、函数非左值引用的返回值),程序不能对这样的右值进行引用获得其地址。

引入右值引用的目的之一是实现移动语义。

(1)移动语义的引入是为了解决在进行大数据复制的时候,将动态申请的内存空间的所有权直接转让出去,不用进行大量的数据移动,既节省空间又提高效率

C++11新特性之0——移动语义、移动构造函数和右值引用

要实现移动语义,就必须让编译器知道什么时候复制,什么时候移动语义,而这就是右值引用发挥作用的地方。移动语义可能修改右值的值,所以,右值引用参数不能是const

(2)通过构造复制构造函数来实现复制语义,通过移动构造函数来实现移动语义。复制构造使用const &引用,而移动构造函数使用非const && 引用

(3)被移动语义的数据交出了所有权,为了不出现析构两次同一数据区,要将交出所有权的数据的指向动态申请内存区的指针赋值位nullptr,即空指针,对空指针执行delete[]是合法的。

A(A && h) : a(h.a)
{
h.a = nullptr; //C++11 新的空指针表示nullptr
}

编译器判断构造函数中是左值还是右值,然后调用相应的复制构造函数或者移动构造函数来构造数据。

(4)移动赋值操作符:他的原理跟移动构造函数相同,如下。

A & operator = (A&& h)
{
assert(this != &h); a = nullptr;
a = move(h.a);
h.a = nullptr;
return *this;
}

(5)强制移动,就是让左值使用移动构造函数,强制让其交出所有权。Utility文件中声明,std::move()函数(将左值强制转换成右值引用)。

(6)这里要注意的是异常发生的情况,要尽量保证移动构造函数不发生异常,可以通过noexcept关键字,这里可以保证移动构造函数中抛出来的异常会直接调用terminate终止程序。详见《C++ Primer Plus》

总结:利用匿名的变量,让其交出所有权,避免复制数据,可以提高程序的效率,因此,如果一个临时变量再也用不着了,可以让其强制移动语义,这样,程序不用再进行大量的数据复制了,尤其是在vector作为返回值的时候。

《C++ Primer Plus》涉及右值引用:P626.const & 和临时变量;p695.decltype(C++11 新. 类型推断)

【下文转自】http://blog.csdn.net/yusiguyuan/article/details/38616821

1、右值引用引入的背景

临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题。但是C++标准允许编译器对于临时对象的产生具有完全的*度,从而发展出了Copy Elision、RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝。下面简单地介绍一下Copy Elision、RVO,对此不感兴趣的可以直接跳过:

(1) Copy Elision

Copy Elision技术是为了防止某些不必要的临时对象产生和拷贝,例如:

struct A {
A(int) {}
A(const A &) {}
};
A a = ;

理论上讲,上述A a = 42;语句将分三步操作:第一步由42构造一个A类型的临时对象,第二步以临时对象为参数拷贝构造a,第三步析构临时对象。如果A是一个很大的类,那么它的临时对象的构造和析构将造成很大的内存开销。我们只需要一个对象a,为什么不直接以42为参数直接构造a呢?Copy Elision技术正是做了这一优化。

【说明】:你可以在A的拷贝构造函数中加一打印语句,看有没有调用,如果没有被调用,那么恭喜你,你的编译器支持Copy Elision。但是需要说明的是:A的拷贝构造函数虽然没有被调用,但是它的实现不能没有访问权限,不信你将它放在private权限里试试,编译器肯定会报错。

(2) 返回值优化(RVO,Return Value Optimization)

返回值优化技术也是为了防止某些不必要的临时对象产生和拷贝,例如:

struct A {
A(int) {}
A(const A &) {}
};
A get() {return A();}
A a = get();

理论上讲,上述A a = get();语句将分别执行:首先get()函数中创建临时对象(假设为tmp1),然后以tmp1为参数拷贝构造返回值(假设为tmp2),最后再以tmp2为参数拷贝构造a,其中还伴随着tmp1和tmp2的析构。如果A是一个很大的类,那么它的临时对象的构造和析构将造成很大的内存开销。返回值优化技术正是用来解决此问题的,它可以避免tmp1和tmp2两个临时对象的产生和拷贝。

【说明】: a)你可以在A的拷贝构造函数中加一打印语句,看有没有调用,如果没有被调用,那么恭喜你,你的编译器支持返回值优化。但是需要说明的是:A的拷贝构造函数虽然没有被调用,但是它的实现不能没有访问权限,不信你将它放在private权限里试试,编译器肯定会报错。

b)除了返回值优化,你可能还听说过一个叫具名返回值优化(Named Return Value Optimization,NRVO)的优化技术,从程序员的角度而言,它其实跟RVO同样的逻辑。只是它的临时对象具有变量名标识,例如修改上述get()函数为:

A get() {
A tmp(); // #1
// do something
return tmp;
}
A a = get(); // #2

想想上述修改后A类型共有几次对象构造?虽然#1处看起来有一次显示地构造,#2处看起来也有一次显示地构造,但如果你的编译器支持NRVO和Copy Elision,你会发现整个A a = get();语句的执行过程,只有一次A对象的构造。如果你在get()函数return语句前打印tmp变量的地址,在A a = get();语句后打印a的地址,你会发现两者地址相同,这就是应用了NRVO技术的结果。

(3) Copy Elision、RVO无法避免的临时对象的产生和拷贝

虽然Copy Elision和NVO(包括NRVO)等技术能避免一些临时对象的产生和拷贝,但某些情况下它们却发挥不了作用,例如:

template <typename T>
void swap(T& a, T& b) {
T tmp(a);
a = b;
b = tmp;
}

我们只是想交换a和b两个对象所拥有的数据,但却不得不使用一个临时对象tmp备份其中一个对象,如果T类型对象拥有指向(或引用)从堆内存分配的数据,那么深拷贝所带来的内存开销是可以想象的。为此,C++11标准引入了右值引用,使用它可以使临时对象的拷贝具有move语意,从而可以使临时对象的拷贝具有浅拷贝般的效率,这样便可以从一定程度上解决临时对象的深度拷贝所带来的效率折损。

2、C++03标准中的左值与右值

要理解右值引用,首先得区分左值(lvalue)和右值(rvalue)。

C++03标准中将表达式分为左值和右值,并且“非左即右”:

Every expression is either an lvalue or an rvalue.

区分一个表达式是左值还是右值,最简便的方法就是看能不能够对它取地址:如果能,就是左值;否则,就是右值。

【说明】:由于右值引用的引入,C++11标准中对表达式的分类不再是“非左即右”那么简单,不过为了简单地理解,我们暂时只需区分左值右值即可,C++11标准中的分类后面会有描述。

3、右值引用的绑定规则

右值引用(rvalue reference,&&)跟传统意义上的引用(reference,&)很相似,为了更好地区分它们俩,传统意义上的引用又被称为左值引用(lvalue reference)。下面简单地总结了左值引用和右值引用的绑定规则(函数类型对象会有所例外):

(1)非const左值引用只能绑定到非const左值;
(2)const左值引用可绑定到const左值、非const左值、const右值、非const右值;
(3)非const右值引用只能绑定到非const右值;
(4)const右值引用可绑定到const右值和非const右值。

测试例子如下:

struct A { A(){} };
A lvalue; // 非const左值对象
const A const_lvalue; // const左值对象
A rvalue() {return A();} // 返回一个非const右值对象
const A const_rvalue() {return A();} // 返回一个const右值对象 // 规则一:非const左值引用只能绑定到非const左值
A &lvalue_reference1 = lvalue; // ok
A &lvalue_reference2 = const_lvalue; // error
A &lvalue_reference3 = rvalue(); // error
A &lvalue_reference4 = const_rvalue(); // error // 规则二:const左值引用可绑定到const左值、非const左值、const右值、非const右值
const A &const_lvalue_reference1 = lvalue; // ok
const A &const_lvalue_reference2 = const_lvalue; // ok
const A &const_lvalue_reference3 = rvalue(); // ok
const A &const_lvalue_reference4 = const_rvalue(); // ok // 规则三:非const右值引用只能绑定到非const右值
A &&rvalue_reference1 = lvalue; // error
A &&rvalue_reference2 = const_lvalue; // error
A &&rvalue_reference3 = rvalue(); // ok
A &&rvalue_reference4 = const_rvalue(); // error // 规则四:const右值引用可绑定到const右值和非const右值,不能绑定到左值
const A &&const_rvalue_reference1 = lvalue; // error
const A &&const_rvalue_reference2 = const_lvalue; // error
const A &&const_rvalue_reference3 = rvalue(); // ok
const A &&const_rvalue_reference4 = const_rvalue(); // ok // 规则五:函数类型例外
void fun() {}
typedef decltype(fun) FUN; // typedef void FUN();
FUN & lvalue_reference_to_fun = fun; // ok
const FUN & const_lvalue_reference_to_fun = fun; // ok
FUN && rvalue_reference_to_fun = fun; // ok
const FUN && const_rvalue_reference_to_fun = fun; // ok

【说明】:(1) 一些支持右值引用但版本较低的编译器可能会允许右值引用绑定到左值,例如g++4.4.4就允许,但g++4.6.3就不允许了,clang++3.2也不允许,据说VS2010 beta版允许,正式版就不允许了,本人无VS2010环境,没测试过。

(2)右值引用绑定到字面值常量同样符合上述规则,例如:int &&rr = 123;,这里的字面值123虽然被称为常量,可它的类型为int,而不是const int。对此C++03标准文档4.4.1节及其脚注中有如下说明:

    If T is a non-class type, the type of the rvalue is the cv-unqualified version of T.
    In C++ class rvalues can have cv-qualified types (because they are objects). This differs from ISO C, in which non-lvalues never have cv-qualified types.

因此123是非const右值,int &&rr = 123;语句符合上述规则三。

此,我们已经了解了不少右值引用的知识点了,下面给出了一个完整地利用右值引用实现move语意的例子:

#include <iostream>
#include <cstring> #define PRINT(msg) do { std::cout << msg << std::endl; } while(0) template <class _Tp> struct remove_reference {typedef _Tp type;};
template <class _Tp> struct remove_reference<_Tp&> {typedef _Tp type;};
template <class _Tp> struct remove_reference<_Tp&&> {typedef _Tp type;}; template <class _Tp>
inline typename remove_reference<_Tp>::type&& move(_Tp&& __t) {
typedef typename remove_reference<_Tp>::type _Up;
return static_cast<_Up&&>(__t);
} class A {
public:
A(const char *pstr) {
PRINT("constructor");
m_data = (pstr != ? strcpy(new char[strlen(pstr) + ], pstr) : );
}
A(const A &a) {
PRINT("copy constructor");
m_data = (a.m_data != ? strcpy(new char[strlen(a.m_data) + ], a.m_data) : );
}
A &operator =(const A &a) {
PRINT("copy assigment");
if (this != &a) {
delete [] m_data;
m_data = (a.m_data != ? strcpy(new char[strlen(a.m_data) + ], a.m_data) : );
}
return *this;
}
A(A &&a) : m_data(a.m_data) {
PRINT("move constructor");
a.m_data = ;
}
A & operator = (A &&a) {
PRINT("move assigment");
if (this != &a) {
m_data = a.m_data;
a.m_data = ;
}
return *this;
}
~A() { PRINT("destructor"); delete [] m_data; }
private:
char * m_data;
}; void swap(A &a, A &b) {
A tmp(move(a));
a = move(b);
b = move(tmp);
} int main(int argc, char **argv, char **env) {
A a(""), b("");
swap(a, b);
return ;
}

输出结果为:

constructor
constructor
move constructor
move assigment
move assigment
destructor
destructor
destructor