本部分的内容包括右值引用(rvalue reference)以及move语义的初步介绍。
一、Rvalue references
Rvalue references are a new reference type introduced in C++ that help solve the problem of unnecessary copying and enable perfect forwarding. When the right-hand side of an assignment is an rvalue, then the left-hand side object can steal resources from the right-hand side object rather than performing a seperate allocation, thus enabling move semantics.
(注:perfect forwarding被翻译为精确传递,精确转发等,其含义为将一组参数原封不动地传递给另一个函数。在这里“原封不动”不仅仅是参数的值不变,除了参数值之外,还有两组属性:左值 / 右值和 const / non-const, 精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。第二部分也会具体解释。另参见https://blog.csdn.net/jxianxu/article/details/62046839)
1. 左值和右值的定义
lvalue:可以出现在operator=左边的值;rvalue:只能出现在operator=右边的值。
比如说临时对象就是一种右值,形似:
int a = 0, b = 0;
a + b = 4;
不能通过编译。但例外的是,很多class的临时对象竟然可以被赋值,以string为例:
string s1("Hello ");
string s2("World");
s1 + s2 = s2; //竟然可以通过编译
//赋值之后s1, s2的值不变
string() = "World"; //竟然可以对temp obj赋值
但是这只是一种例外情况,可以理解为语言本身的“bug”,临时对象是永远被当做右值的,即使有些class的临时对象可以放在等号左边(“可以”指可以通过编译,给临时对象赋值无意义)。
2. 为什么要引入一个新的"右值引用"的概念
当rvalue出现于operator=(copy assignment)的右侧,我们认为对其资源进行偷取 / 搬移(move)而非拷贝(copy)是可以的,是合理的,那么:
(1)必须有语法让我们在调用端告诉编译器:这是个rvalue
(2)必须有语法让我们在被调用端写出一个专门处理rvalue的所谓move assignment函数
3. 一个例子
(1)insert()往容器中插入元素,涉及到元素的移动,所以为了效率insert()有右值引用的版本,搬动元素的时候要调用元素的ctor,调用的ctor就是元素的move ctor。
(2)一个对象的move ctor的逻辑:简单地copy 指针的值,所以原来的对象对资源的引用要销毁,要保证原来的对象不再使用,这样才安全
(3)使用std::move()可以得到一个左值的右值引用
二、Perfect forwarding
这里只说了怎么写可以实现完美转发,但是没有解释为什么需要这么写?以及各种不同类型(左值还是右值,const还是non-const)的情况是怎样得到正确处理的?得去别的地方找一下。
下面是标准库中forward()和move()的定义:
//Forward on lvalue
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
//Forward on rvalue
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
三、写一个move-aware class
class MyString{
public:
static size_t DCtor; //累计default-ctor呼叫次数
static size_t Ctor; //累计ctor呼叫次数
static size_t CCtor; //累计copy-ctor呼叫次数
static size_t CAsgn; //累计copy-asgn呼叫次数
static size_t MCtor; //累计move-ctor呼叫次数
static size_t MAsgn; //累计move-asgn呼叫次数
static size_t Dtor; //累计dtor呼叫次数
private:
char* _data;
size_t _len;
void init_data(const char *s){
_data = new char[_len + 1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
//default constructor
MyString() : _data(NULL), _len(0) { ++DCtor; }
//constructor
MyString(const char* p) : _len(strlen(p)){
++Ctor;
_init_data(p);
}
//copy constructor
MyString(const MyString& str) : _len(str.len){
++CCtor;
_init_data(str._data);
}
//move constructor, with "noexcept"
MyString(MyString&& str) noexcept
: _data(str._data), _len(str._len){
++MCtor;
str._len = 0;
str.data = NULL; //重要
}
//copy assignment
MyString& operator=(const MyString& str){
++CAsgn;
if(this != &str){
if(_data) delete _data;
_len = str._len;
_init_data(str._data);
}
else{
}
return *this;
}
//move assignment
MyString& operator=(MyString&& str) noexcept{
++MAsgn;
if(this != &str){
if(_data) delete _data;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
return *this;
}
//dtor
virtual ~MyString(){
++Dtor;
if(_data){
delete _data;
}
}
bool
operator<(const MyString& rhs) const
{
return string(this->_data) < string(rhs._data);
}
bool
operator==(const MyString& rhs) const
{
return string(this->_data) == string(rhs._data);
}
char* get() const { return _data; }
};
size_t MyString::DCtor = 0;
size_t MyString::Ctor = 0;
size_t MyString::CCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MCtor = 0;
size_t MyString::MAsgn = 0;
size_t MyString::Dtor = 0;
namespace std //必须放在std内
{
template<>
struct hash<MyString>{ //for unordered containers
size_t
operator()(const MyString& s) const noexcept
{ return hash<string>() (string(s.get())); }
};
(1)定义了Big5和hashfunction,拷贝构造和拷贝赋值的逻辑很重要,具体见代码
(2)move constructor和move assignment operator本质上都是浅拷贝,但在浅拷贝完成以后要把原先对象与资源的联系切断,在本例中表现为将指针置为NULL。所以在析构函数中,释放资源之前要先判断指针是否为NULL
四、使用三中定义的MyString测试容器的效能
测试内容:(1)分别使用定义了move语义的和没有定义move语义的class,对容器进行多次插入操作(都只在尾部插入)。(2)测试拷贝容器,move容器,swap两个容器的效率
结果:(1)vector的两种时间差别巨大,因为vector要扩容,所以涉及很多拷贝操作,使用move会大大节省时间,而对于list,deque,multiset和unordered_multiset两种时间差别不大。但其实按理说deque也可能要搬动元素,但本例中插入的元素都在尾部,所以不涉及搬动操作。(2)所有容器都体现出:copy容器耗时很多,move和swap容器几乎不耗时。这是因为copy容器需要分配空间并依次拷贝元素,但是move容器仅仅是交换了三个指针的值(以vector为例,vector中的三个指针分别是首元素迭代器,尾后迭代器,指示最多容纳元素的迭代器),自然快得不得了。swap应该也只是交换了指针。