专业编程基础技术教程

网站首页 > 基础教程 正文

C++模板 - 12(移动语义和std::move)

ccvgpt 2024-08-03 12:31:51 基础教程 19 ℃

这次介绍一个并不是C++模板本身的特性,但是它对程序设计的影响深远。C++11的一个重要特性就是引入了移动语义(move semantic)。在这个之前,类似的返回值优化(RVO)也只是C++的一项编译优化技术。现在我们在代码中可以明确地告诉编译器,这个对象我不打算继续使用了,你可以使用它的资源完成初始化,不需要从头创建。

人有南北,为了适应移动语义的需要,现在值也分左右了。

C++模板 - 12(移动语义和std::move)

int x = 6;

那由谁来分呢?赋值操作符,左边的是左值,右边的是右值。x也可以放在右边,用来给其他变量赋值,那个时候它就是右值了。这里可以看出它们的主要区别:左值看重的是对象的存储空间,因为需要它来装东西,而右值则使用的是它的值。

int& rx = x;
int&& rp = 6;

值分了左右,引用也相应地变成了左值引用&和右值引用&&(这个可不是引用的引用)两种。左值引用的目的是减少对象复制,同时还可以提供参数对象的可修改性(mutable)。右值引用则大大提高了对象创建的效率,减少了临时对象的创建与释放。

class MyClass {
  public:
    MyClass();
    MyClass(const MyClass&);
    MyClass(MyClass&&);
    MyClass& operator=(const MyClass&);
    MyClass& operator=(MyClass&&);
};

为了支持移动语义,原来的构造三件套现在也升级为五件套了,增加了移动构造函数和移动赋值操作,可以直接从参数对象里面“偷取”资源来完成对象的创建(创建完成后,参数引用的对象基本上就废了,是个moved-form对象,通常只剩下被析构了,如果再次使用可能会导致未定义行为)。

void g(MyClass& lrc) {
    std::cout << "g(MyClass& c)\n";
}

void g(MyClass&& rrc) {
    std::cout << "g(MyClass&& c)\n";
}

void f(MyClass&& rrc) {
    g(rrc);
}

我们定义了两个g(),一个参数是左值引用,另一个是右值引用。那f()中的g()函数调用会使用哪个版本呢?答案好像是显而易见的,既然rrc是个右值引用,当然是使用第二个版本了。错了,这里匹配的是第一个版本。有没有搞错?没有,而且这个居然是C++有意这样做的。

rrc的类型是个MyClass类型的右值引用,这个没错,但是rrc用在表达式时却是个左值(这里是用作函数的调用参数,换作其他的表达式也一样),原因是如果右值引用直接用作右值,那rrc这个参数在函数体内只能被使用一次,之后再次使用都会将程序置于危险境地,因为它已经是个moved-form对象了(比如说假设MyClass支持+操作,那rrc+rrc后,程序基本上就挂了)。

那问题是我怎么才能让它继续它的右值之旅呢?毕竟它传进来的时候说好了我可以随意处置它的资源的。在你确定不会再继续使用它时,最后一次这样使用:g(std::move(rrc)); 这个时候就会调用g()函数的第二个版本了。

好神奇啊,std::move到底移动了什么呢?其实什么也没有,它仅仅是个cast,将一个左值强制转换为右值引用(static_cast<MyClass&&>(rrc)),转换之后,rrc再次回到了它的右值引用了(右值其实分为两种:pvalue,“纯右值”,中间临时对象,包括6这样的值都是,这些是编译器本身就知道的;xvalue,“将死值”,经过std::move强制转换的对象,这些是需要明确告诉编译器的,不然编译器并不敢直接拿来用)。

移动语义的出现对程序设计的影响很大,之前的规则是函数参数尽量使用引用,特别是一些大的对象,减少不必要的对象复制,但引用本身的不确定性有时也让人头疼。现在两全其美的方式来了,我们可以再次回到值语义,用户调用时如果参数对象不再使用了,可以直接move过去。当然你如果确定参数引用的对象生存期不会有问题,直接使用引用还是可以省去一次移动构造的,虽然后者的开销非常小。

Tags:

最近发表
标签列表