C/C++教程

C++右值引用和移动构造函数

本文主要是介绍C++右值引用和移动构造函数,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

对象的拷贝

C++新标准之前对象的拷贝控制由拷贝构造函数,重载的拷贝赋值运算符,析构函数三个函数决定。
新标准之后新增两个函数:移动构造函数,移动赋值运算符

左值和右值

左值(lvalue) 指持久存在的对象或返回值类型为左值引用的返回值,是不可移动的。

右值(rvalue) 包含了临时对象或者返回值为右值引用的返回值,是可移动的。

  • 形式为cast-name(exp),其中type是转换的类型,exp是转换的值,如果type是引用类型,则结果是左值
  • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值
  • 内置的解引用运算符、下标运算符、迭代器解引用运算符、容器的下标运算符的求值结果都是左值
  • 内置类型和迭代器的递增递减运算符作用于左值对象,所得的结果也是左值

右值引用

为了支持移动操作,C++新标准引入了一种新的引用类型——右值引用&&,即必须绑定到右值的引用

int &&i = 42;		//正确
int j = 42;
int &&k = j;		//错误,右值引用不能绑定到左值
const int &r = j*42;	//正确,const会创建一个临时的const变量
int &r1 = j*42;		//错误,右值引用不能绑定到左值
int &&r2 = j*42;	//正确,j*42是一个右值
int &&r3 = i;		//错误,表达式i是一个左值

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符都是返回左值表达式的例子。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值,不能将左值引用绑定到这类表达式,但可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

强制转换右值move函数

有时候我们需要将左值像右值一样转移所有权

void func(){
	A res;
    //.....
    if(xxx)
        ans = res;	//我希望将res转移到外部变量ans上
    return;
}

在上述代码中,res赋值给ans之后不再被使用,我们希望调用的是移动赋值构造函数。

但是res是一个左值,因此ans = res调用的是赋值构造函数。

为了将某些左值当成右值使用,C++新标准提供了 std::move 函数以用于将某些左值转成右值,以匹配右值引用类型。

void func(){
	A res;
    //.....
    if(xxx)
        ans = std::move(res);	//这时候调用的是移动赋值构造函数
    return;
}

函数参数传递

void func1(A a) {return;}
void func2(A &&a) {return;}

int main() {
	A a;
	A &b = a;
	A c;
	A d;

    //请回答:不开优化的版本下,调用以下函数分别有多少Copy Consturct、Move Construct的开销?
	func1(a);		//调用拷贝构造函数
	func1(b);		//调用拷贝构造函数
	func1(std::move(c));	//调用移动构造函数
	func2(std::move(d));	//都不调用
}

实际上在不开优化的版本下,如果实参为右值,调用func1的开销只比func2多了一次移动构造函数和析构函数。

实参传递给形参,即形参会根据实参来构造。其结果是调用了移动构造函数;函数结束时则释放形参。

倘若说对象的移动构造函数开销较低(例如内部仅一个指针属性),那么使用无引用类型的形参函数是更优雅的选择,而且还能接受左值引用类型或无引用的实参(尽管这两种实参都会导致一次Copy Consturct)。可以说,这种情况下,只提供非引用类型的版本,也是可以接受的。

从极致的优化角度来看,如果参数有支持移动构造(或移动赋值)的类型,应该同时提供左值引用(匹配左值)和右值引用(匹配右值)两种重载版本。

函数返还值传递

A func1()
{
    A a;
    return a;
}

A func2()
{
    A a;
    return std::move(a);
}

A &&func3()
{
    A a;
    return std::move(a);
}

int main()
{
    A test1 = func1();
    //test1调用情况 Construct func
    A test2 = func2();
    //test2调用情况 Construct func Move func Destroy func
    A test3 = func3();
    //test3调用情况 Construct func Destroy func Move func
    return 0;
}

执行这3行代码实际上都没有任何Copy Construct的开销,并且func3是危险的。因为局部变量释放后,函数返还值仍持有它的右值引用。

因此,不建议函数返还右值引用类型,同前面传递参数类似的,移动构造开销不大的时候,直接返还非引用类型就足够了(在某些特殊场合有特别作用,准确来说一般用于表示返还成一个右值,如std::move的实现)。

结论:

1. 我们应该首先把编写右值引用类型相关的任务重点放在对象的构造、赋值函数上。从源头上出发,在编写其它代码时会自然而然享受到了移动构造、移动赋值的优化效果。

2. 形参:从优化的角度上看,若参数有支持移动构造(或移动赋值)的类型,应提供左值引用和右值引用的重载版本。移动开销很低时,只提供一个非引用类型的版本也是可以接受的。

3. 返还值:不要且没必要编写返还右值引用类型的函数,除非有特殊用途。

参考:
1. <<C++ Primer>>第五版
2. 透彻理解C++11 移动语义:右值、右值引用std::move、std::forward

这篇关于C++右值引用和移动构造函数的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!