右值引用&&

Posted 清水寺扫地僧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了右值引用&&相关的知识,希望对你有一定的参考价值。



一、新特性的目的

右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。即可用来实现转移构造函数和转移赋值运算符;
  • 能够更简洁明确地定义泛型函数;


二、何为右值

C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。 所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。 右值是指临时的对象,它们只在当前的语句中有效。

int i = 0;  // 在这条语句中,i 是左值,0 是临时值,就是右值。

在C++11之前,右值是不能被引用的,如:

int &a = 1;   // error C2440: “初始化”: 无法从“int”转换为“int &”

我们最多只能用常量引用来绑定一个右值,如:

const int &a = 1;

在C++11中,我们可以引用右值,使用&&来实现:

int &&a = 1;


三、转移构造函数/转移赋值运算符重载

有如下string类,实现了拷贝构造函数和赋值运算符重载。

class MyString {
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:
	MyString() {
		_data = NULL;
		_len = 0;
	}

	MyString(const char* p) {
		_len = strlen(p);
		_init_data(p);
	}

	MyString(const MyString& str) {
		_len = str._len;
		_init_data(str._data);
		std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
	}

	MyString& operator=(const MyString& str) {
		if (this != &str) {
			_len = str._len;
			_init_data(str._data);
		}
		std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
		return *this;
	}

	virtual ~MyString() {
		if (_data != NULL) {
			std::cout << "Destructor is called! " << std::endl; 
			free(_data);
		}
	}
};

int main() { 
	MyString a; 
	a = MyString("Hello"); 
	std::vector<MyString> vec; 
	vec.push_back(MyString("World")); 
}

运行结果:

Copy Assignment is called! source: Hello
Destructor is called!
Copy Constructor is called! source: World
Destructor is called!
Destructor is called!
Destructor is called!

总共执行了2次拷贝,MyString("Hello")MyString("World")都是临时对象,临时对象被使用完之后会被立即析构,在析构函数中free掉申请的内存资源。如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

通过加入定义转移构造函数转移赋值操作符重载来实现右值引用(即复用临时对象):


	MyString(MyString&& str) { 
		std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
		_len = str._len; 
		_data = str._data; 
		str._len = 0; 
		str._data = NULL;   // ! 防止在析构函数中将内存释放掉
	}

	MyString& operator=(MyString&& str) { 
		std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
		if (this != &str) { 
			_len = str._len; 
			_data = str._data; 
			str._len = 0; 
			str._data = NULL;  // ! 防止在析构函数中将内存释放掉
		} 
		return *this; 
	}

转移构造函数和转移赋值运算符重载只能接受右值参数,而函数返回值是右值,所以就会依据函数签名匹配到转移构造函数和转移赋值运算符重载上。

运行结果:

Move Assignment is called! source: Hello
Move Constructor is called! source: World
Destructor is called!
Destructor is called!

需要注意的是:右值引用并不能阻止编译器在临时对象使用完之后将其释放掉的事实,所以转移构造函数转移赋值操作符重载函数 中都将_data赋值为了NULL,而且析构函数中保证了_data != NULL才会释放。



四、标准库函数 std::move :将左值命名对象转换为右值

既然编译器只对右值引用才能调用转移构造函数转移赋值函数,又因为所有命名对象都只能是左值引用。 在这样的条件下,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

void ProcessValue(int& i) { 
	std::cout << "LValue processed: " << i << std::endl; 
} 

void ProcessValue(int&& i) { 
	std::cout << "RValue processed: " << i << std::endl; 
} 

int main() { 
	int a = 0; 
	ProcessValue(a); 
	ProcessValue(std::move(a)); 
}

运行结果:

LValue processed: 0 
RValue processed: 0

std::move在提高 swap 函数的的性能上非常有帮助,一般来说,swap函数的通用定义如下:

template <class T> 
void swap(T& a, T& b) 
{ 
	T tmp(a);   // copy a to tmp 
	a = b;      // copy b to a 
	b = tmp;    // copy tmp to b 
}

有了std::move,再结合右值引用,就可以避免不必要的拷贝了。 swap函数的定义变为 :

template <class T>
void swap(T& a, T& b) 
{ 
	T tmp(std::move(a)); // move a to tmp 
	a = std::move(b);    // move b to a 
	b = std::move(tmp);  // move tmp to b 
}

可以使用第三节中的MyString类进行测试:

int main() { 
	MyString a("a");
	MyString b("b");

	swap(a, b);

	return 0;
}


五、完美转发/精确传递(Perfect Forwarding)

完美转发/精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。在泛型函数中,这样的需求非常普遍。 举例说明比较好理解。

forward_value函数只有一个参数val,定义如下:

template <typename T> 
void forward_value(const T& val) { 
	process_value(val); 
} 

template <typename T> 
void forward_value(T& val) { 
	process_value(val); 
}

函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&,否则,下面四种不同类型参数的调用中就不能同时满足,由此在泛型编程中引入了右值引用使得实现更为简洁明确:

int a = 0; 
const int &b = 1; 
forward_value(a); // int& 
forward_value(b); // const int& 
forward_value(2); // const int&

对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。我们看看右值引用如何帮助我们解决这个问题:

template <typename T> 
void forward_value(T&& val) { 
	process_value(val); 
}

只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。

经测试,VS2015已经支持右值引用&&。

参考:https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html



六、折叠引用/引用坍塌(reference-collapsing rules)

感觉上叫 引用坍塌 好理解点(reference-collapsing rules), 就这样:

A& & 变成 A&
A& && 变成 A&
A&& & 变成 A&
A&& && 变成 A&&

就是左值引用会传染,只有纯右值&& && = &&,沾上一个左值引用就变左值引用了,更具体的规则如下:

引用折叠的规则上面的各位已经说得很清楚了,我在这里补充下引用折叠是为什么出现的吧。

要说引用折叠,首先得说右值引用(在看这个之前需要了解C++11中左值,右值的概念)。它是C++11出现的新概念,声明类型的方法是:T&&,具体信息可以看下面的代码:

Class A {
    A() {// do something}
};
 
A GetA() {
    return A();
}
 
int main() {
    A a1 = GetA();   // a1是左值
    A&& a2 = GetA(); // a2是右值引用
    return 0;
}

a1是左值,在构造时使用了GetA() 产生的临时对象,之后GetA()产生的临时对象会销毁。

a2是右值引用,其指向的就是GetA()所产生的对象,这个对象的声明周期是和a2的声明周期是一致的。即少了临时对象,从而省去了临时对象的构造和析构。

由此可见右值引用的好处,在新代码中,右值引用是值得大力使用的。但是,在使用的时候,有例外情况了:T&&并不是一定表示右值,比如,如果它绑定的类型是未知的话,既可能是左值,又可能是右值。比如:

template<typename T>
void f(T&& param);
 
f(10); // 10是右值
int x = 10;
f(x);  // x是左值

以上这种未定的引用类型(param的类型)称为 universal references,这种类型必须被初始化,而它是左值还是右值则取决于它的初始化,如果被左值初始化,那么它就是左值,反之亦然。那么什么时候是左值,什么时候是右值,就需要进行类型推导才知道。

由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或右值引用的参数初始化,这是经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化就称为引用折叠。(《深入应用C++11-代码优化与工程级应用》 — 祁宇 P68 )

引用折叠的规则如下(配合@jun-jun的答案)[和上一段的出处一样]:

  • 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&)
  • 所有的其他引用类can型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)

参考:https://www.zhihu.com/question/40346748/answer/88672920

以上是关于右值引用&&的主要内容,如果未能解决你的问题,请参考以下文章

[C++11]右值和右值引用

右值引用 && 的另一个优化。只需重命名字段

右值引用、复制和移动

C11新特性右值引用&&

为什么传递右值引用(X &&)是AS IF传递左值引用(X&)?

C++11 std::forward(配合&&右值引用)