13.6 对象移动

  新标准的一个最主要的特性是可以移动而非拷贝对象的能力,在某些情况下,移动而非拷贝对象会大幅度提升性能。

右值引用

  为了支持移动操作,新标准引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

  一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。对于常规引用,我们可以称之为左值引用,我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式:

int i=42; 
int &r=i;  //正确:r引用i
int &&rr=i;  //错误:不能将一个右值引用绑定到一个左值上
int &r2=i*42;  //错误:i*42是一个右值
const int &r3=i*42;  //正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2=i*42;  //正确:将rr2绑定到乘法结果上

  左值持久,右值短暂
  左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。由于右值引用只能绑定到临时对象,我们得知:
  1、所引用的对象将要被销毁
  2、该对象没有其他用户

  变量是左值
  变量可以看作只有一个运算对象而没有运算符的表达式,变量表达式也有左值/右值属性,变量表达式都是左值。意味着我们不能将一个右值引用绑定到一个右值引用类型的变量上:

int &&rr1=42;  //正确:字面常量是右值
int &&rrr2=rr1;  //错误:表达式rrr1是左值

  标准库move函数
  我们可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用:

int &&rr3=std::move(rr1);  //ok

  move告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能将对移后源对象的值做任何假设。

移动构造函数和移动赋值运算符

  类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。

StrVec::StrVec(StrVec &&s) noexcept  //移动操作不应抛出任何异常
    //成员初始化器接管s中的资源
    : elements(s.elements),first_free(s.first_free),cap(s.cap)
{
      //令s进入这样的状态——对其运行析构函数是安全的
      s.elements=s.first_free=s.cap=nullptr;
}

  与拷贝构造函数不同,移动构造函数不分配任何新内存;它接管StrVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作,此对象将继续存在,最终,移后源对象会被销毁,意味着将在其上运行析构函数。

  移动操作、标准库容器和异常
  由于移动操作“窃取”资源,因此,移动操作通常不会抛出任何异常。当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。一种通知标准库的方法是在我们的构造函数中指明noexcept。我们在一个函数的参数列表后指定noexcept,我们必须在类头文件的声明和定义中(如果定义在类外的话)都指定noexcept。

  移动赋值运算符
  移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正常处理自赋值:

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
      //直接检测自赋值
      if(this!=&rhs) {
         free();  //释放已有元素
         elements=rhs.elements;  //从rhs接管资源
         first_free=rhs.first_free;
         cap=rhs.cap;
         //将rhs置于可析构状态
         rhs.elements=rhs.first_free=rhs,cap=nullptr;
      }
      return *this;
}

  移后源对象必须可析构
  从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。

  合成的移动操作
  与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符,但是合成移动操作的条件与合成拷贝操作的条件大不相同。

  与拷贝操作不同,编译器根本不会为某些类合成移动操作。特别是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。

  只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。

//编译器会为X和hasX合成移动操作
struct X {
      int i;  //内置类型可以移动
      std::string s;  //string定义了自己的移动操作
};
struct hasX {
      X mem;  //X有合成的移动操作
};
X x,x2=std::move(x);  //使用合成的移动构造函数
hasX hx,hx2=std::move(hx);  //使用合成的移动构造函数

  定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。

  移动右值,拷贝左值······
  如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作的情况类似:

StrVec v1,v2; 
v1=v2;  //v2是左值;使用拷贝赋值
StrVec getVec(istream &);  //getVec返回一个右值
v2=getVec(cin);  //getVec(cin)是一个右值;使用移动赋值

  ······但如果没有移动构造函数,右值也被拷贝
  如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使我们试图通过调用move来移动它时也是如此:

Foo z(std::move(x));  //拷贝构造函数,因为未定义移动构造函数

  一般情况下,拷贝构造函数满足对应的移动构造函数的要求;它会拷贝给定对象,并将原对象置于有效状态。

  拷贝并交换赋值运算符和移动操作

class HasPtr {
public:
      //添加的移动构造函数
      HasPtr(HasPtr &&p) noexcept : ps(p.ps),i(p.i) {p.ps=0;}
      //赋值运算符既是移动赋值运算符,也是拷贝赋值运算符
      HasPtr& operator=(HasPtr rhs)
                                { swap(*this,rhs); return *this;}
      //其他成员的定义
};

  假定hp和hp2都是HasPtr对象:

hp=hp2;  //hp2是一个左值;hp2通过拷贝构造函数来拷贝
hp=std::move(hp2);  //移动构造函数移动hp2

  在第一个赋值中,右侧运算对象是一个左值,因此移动构造函数是不可行的。rhs使用拷贝构造函数来初始化。在第二个赋值中,我们调用std::move将一个右值引用绑定到hp2上。在此情况下,拷贝构造函数和移动构造函数都是可行的。但是由于实参是一个右值引用,移动构造函数是精确匹配的。移动构造函数从hp2拷贝指针,而不会分配任何内存。

  移动迭代器
  新标准库中定义了一种移动迭代器适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用。

  我们通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。

void StrVec::reallocate()
{
    //分配大小两倍与当前规模的内存空间
    auto newcapacity=size() ? 2 * size() : 1;
    auto first=alloc.allocate(newcapacity);
    //移动元素
    auto last=uninitialized_copy(make_move_iterator(begin()),
                                  make_move_iterator(end()),first);
    free();  //释放旧空间
    elements=first;  //更新指针
    first_free=last;  
    cap=elements+newcapacity;
}

  uninitialized_copy对输入序列中的每个元素调用construct来将元素“拷贝”到目的位置。此算法使用迭代器的解引用运算符从输入序列中提取元素。由于我们传递给它的是移动迭代器,因此解引用的运算符生成的是一个右值引用,这意味着construct将使用移动构造函数来构造元素。

右值引用和成员函数

  如果一个成员函数同时提供拷贝和移动版本,它通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式——一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值引用。例如:

void push_back(const X&);  //拷贝:绑定到任意类型的X
void push_back(X&&);  //移动:只能绑定到类型X的可修改的右值

  右值和左值引用成员函数

s1+s2="wow";

  此处我们对两个string的连接结果——一个右值,进行了赋值。在旧标准中,我们没有办法组织这种使用方式,为了维持向后兼容性,新标准库类任然允许向右值赋值。但是,我们可能希望在自己的类中阻止这种用法。在此情况下,我们希望强制左侧运算对象(即this指向的对象)是一个左值。我们指出this的左值/右值属性的方式与定义const成员函数相同,即在参数列表后放置一个引用限定符:

class Foo {
public:
     Foo &operator=(const Foo&) &;  //只能向可修改的左值赋值
    //Foo的其他参数
};

  引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。

  一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟随在const限定符之后:

Foo someMem() const &;

  重载和引用函数
  引用限定符也可以区分重载版本。而且,我们可以综合引用限定符和const来区分一个成员函数的重载版本。
  如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符,否则是错误的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容