右值引用

本文是对《深入理解C++11》和《深入应用C++11:代码优化与工程级应用》中右值引用部分的总结。

概述

在C++11中增加了新的类型---右值引用(T&&)。通过它可以实现移动语义完美转发

作用:

  • 主要是避免无谓的复制,节省运算存储资源,提高程序性能
  • 能够更简洁明确地定义泛型函数。

 

左值&右值

概念:

  • 左值:

能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。

  • 右值:

不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

右值中有两个重要概念:将亡值纯右值

1.  将亡值:C++11新增概念,是指具有转移语义的对象,比如:将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值。

2.  纯右值:是用于识别临时变量和一些不跟对象关联的值。如: 非引用返回的临时变量、运算表达式产生的临时变量、原始字面变量和Lambda表达式等。

 

右值引用

右值引用就是对右值进行引用的类型。

    T && a = ReturnRval();

因为右值不具名,所以只能通过引用方式找到它。作为一个引用类型,在声明时必须立即初始化,通过右值引用的声明,被引用对象生命周期与右值引用类型变量的生命周期一样,可以说右值引用能用来延长右值的生命周期。

 

虽然上述用T&& 来定义右值,但它并不一定只表示右值,它与其绑定的类型相关。

    template
    void f(T&& param);

    f(10);        // 10是右值
    int x = 10;
    f(x);         // x是左值

     由上面例子可以看出:param可能是左值,也可能是右值,这完全取决于在调用函数时所传递的参数类型。在加之有&&修饰所以param也被称作未定义的引用类型(universal references)。

     param必须要被初始化,当由右值初始化时它便是右值,当被左值初始化时它便是左值。

     所以只有&&发生自动类型推断时(函数模板的类型自动推导、auto关键字),&&才是一个未定的引用类型。

总结

  • 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值,左值引用只能接受左值,但常量左值引用(const T& )也称作万能引用类型,它可以接受左值、右值、常量左值、常量右值。
  • auto&& 或 函数自动类型推导的T&& 是一个未定的引用类型,它通过初始化来确定具体的引用类型
  • 引用折叠规则:右值引用叠加到右值引用上仍是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它变成左值引用,输入右值则变为具名的右值引用。
  • 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值

 

移动语义

概念:

是将资源通过浅拷贝方式从一个对象转移到另一个对象。通过此方式可以减少不必要的临时对象的创建、拷贝、销毁,从而提高程序性能。

如下实现一个MyString类: 

class MyString{

private:
	char* m_data;
	size_t m_len;

	void copy_data(const char* s){
		m_data = new char [m_len+1];
		memcpy(m_data, s, m_len);
		m_data[m_len] = '\0';
	}

public:
	MyString(){
		m_data = NULL;
		m_len = 0;
	}

	MyString(const char* p){
		m_len = strlen(p);
		copy_data(p);
	}

	MyString(const MyString& str){
		m_len = str.m_len;
		copy_data(str.m_data);
		std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
	}

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

	virtual ~MyString(){
		if (m_data) free(m_data);
	}

};

void test(){
	MyString a;
	a = MyString("Hello");
}

在MyString("Hello")对a进行初始化时,开始先构造一个临时对象,返回这个临时对象,再通过拷贝赋值函数构造a对象,调用结束后对临时对象进行销毁。

而通过右值引用方式则可以省去多余的构造和销毁。

右值引用代码:

MyString(MyString&& str){
    std::cout << "Move Construct is called! source: " << str.m_data << std::endl;
    m_len = str.m_len;
    m_data = str.m_data;    // 避免了不必要的拷贝
        
    // 资源转移后临时对象不在指向该资源
    str.m_len = 0;
    str.m_data = NULL;
}


MyString& operator=(MyString&& str){
    std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
    if(this != &str){
        m_len = str.m_len;
        m_data = str.m_data;    // 避免了不必要的拷贝
        str.m_len = 0;
        str.m_data = NULL;
    }
    return *this;
}

通过此方式使得临时对象的资源转交给目标对象,而不再是拷贝、销毁,这样既能节省资源,又能省去资源申请和释放的时间。尤其当临时对象申请较多资源时,此方式能大幅度提升程序性能。

深度拷贝与移动语义区别如下图:

右值引用_第1张图片

右值引用_第2张图片

所以在设计和实现类时,如果涉及动态申请大量的资源的类,应该要考虑右值引用的拷贝构造和拷贝赋值,以提高效率。另外需要注意的是除了提供上述函数外,通过也要提供常量左值引用的拷贝构造函数,以保证移动不成还可以拷贝构造。

 

完美转发

首先运行下示程序:

template
void PrintT(T& t){
	cout << "lvalue" << endl;
}

template
void PrintT(T&& t){
	cout << "rvalue" << endl;
}

template
void TestForward(T&& v){
	PrintT(v);
}

void main(){
	TestForward(1);
	int x = 1;
	TestForward(x);
}

运行结果:

右值引用_第3张图片

 x是左值所以很好理解输出结果为lvalue。但1作为右值输出为lvalue是为什么?

因为1是右值,所以未定义的引用类型T&& v被一个右值初始化后变为具名的右值引用,所以在TestForWard函数内调用PrintT(v)时,此时v是具名变量,它是左值,所以被PrintT(T& )所调用,从而结果打印lvalue。(参照之前在右值引用处总结的三、四条)

 

那么我们想要保证参数按照原来类型转发到其他函数,就上例而言,希望v依然作为右值被转发。这种转发被称为完美转发

实现就是靠std::forward函数实现:

template
void TestForward(T&& v){
	//PrintT(v);
	PrintT(std::forward(v));  // forward实现完美转发
}

运行结果:

右值引用_第4张图片

 

你可能感兴趣的:(C++,右值引用)