【C++11】右值引用使用详解

系列文章目录

C++11新特性使用详解-持续更新


文章目录

    • 系列文章目录
    • 前言
    • 一、关联特性
      • 1.1 左值/右值
    • 二、使用方法
      • 2.1 获得右值引用
      • 2.2 对象移动方法
        • 2.2.1 移动构造函数/移动赋值运算符
        • 2.2.2 标记为noexcept
        • 2.2.3 使移动源对象进入是可析构状态
    • 三、使用场景
      • 3.1 移动语义
      • 3.1 完美转发
    • 四、总结


前言

开发中很多情况会发生对象拷贝,在某些情况下,对象拷贝后立即就被销毁了,在这些情况下,从旧内存将元素拷贝到新内存是不必要的。而且C++旧标准中,没有直接的方法可以移动对象,因此不必拷贝的情况下,我们也不得不拷贝。如果对象较大,或者对象本身要求分配内存空间(如string),进行不必要的拷贝代价是非常大的。总之,移动而非拷贝对象会大幅度提升性能。
另外,例如IO类或unique_ptr这样的类,不包含能共享的资源,因此类型的对象不能被拷贝,但可以移动。为了支持移动操作,C++11新标准引入了新的引用类型——右值引用(rvalue references)。所谓右值引用,就是必须绑定到右值的引用。

Note
标准库、string和shared_ptr类既支持移动也支持拷贝。IO和unique_ptr类可以移动但不能拷贝。


一、关联特性

1.1 左值/右值

左值(Lvalue)是指可标识且持久存在的表达式,如变量、函数返回的左值引用等。
右值(Rvalue)是指临时且即将被销毁的表达式,如字面量、临时对象、表达式结果等。

当一个对象被用作右值的时候,用的是对象的值(内容)。 当一个对象被用作左值的时候,用的是对象的身份(在内存中的位置)。


二、使用方法

2.1 获得右值引用

通过是std::move函数来获得绑定到左值上的右值引用。
调用std::move意味着:除了对源对象赋值或销毁它外,我们将不再使用它。在调用后,我们不能对移动后源对象的值作任何假设。

应该使用std::move,而不是move。这样可以避免潜在的名字冲突。

2.2 对象移动方法

2.2.1 移动构造函数/移动赋值运算符

类似string类,为了让自己的类也能支持移动,需要为其定义移动构造函数和移动赋值运算符(移动资源而不是拷贝资源)。

2.2.2 标记为noexcept

如果你的类的移动构造函数可以保证不会抛出异常,建议将其声明为 noexcept,以提供额外的异常安全性保证。这可以帮助确保在移动操作失败时,对象仍然处于有效且可用的状态,提高程序的可靠性和健壮性。

  1. 由于移动操作是窃取资源,它通常不分配任何资源。因此移动操作通常不会抛出任何异常。
  2. 当编写一个不抛出异常的的移动操作时,我们应该将此时通知标准库。否则标准库会认为移动我们的类对象可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。
  3. 当移动构造函数声明为 noexcept 时,它表示移动操作不会抛出异常。这可以提供额外的异常安全性保证,称为强异常安全保证(strong exception safety
    guarantee)。
    强异常安全保证要求在移动构造函数中,如果发生异常,对象的状态不会发生改变,即要么移动操作成功完成,要么对象保持在移动之前的状态。这可以确保在移动操作失败时,对象仍然处于有效且可用的状态,不会导致资源泄漏或不一致的状态。
  4. 如果移动构造函数没有声明为 noexcept,则无法提供强异常安全保证。在移动操作中,如果移动构造函数抛出异常,对象可能会处于部分移动的状态,资源可能会泄漏或对象可能会处于不一致的状态。
2.2.3 使移动源对象进入是可析构状态

通过将源对象的指针置为nullptr来实现源对象进入一个可析构状态。

对象移动并不会销毁此对象,但有时在移动操作后,源对象会被销毁。因此我们必须确保源对象进入一个可析构状态。这样即使源对象被析构,也不会释放对象内存。


三、使用场景

右值引用在以下情况下特别有用:

  1. 移动语义:当需要转移资源所有权而不进行深拷贝时,可以使用右值引用来实现高效的移动语义。
  2. 完美转发:当需要将参数以原样传递给其他函数时(同时保留其值类别(左值或右值))和常量性(const限定符),可以使用右值引用来实现完美转发,可以避免不必要的拷贝和类型转换。在泛型编程中特别有用。
  3. 优化性能:通过使用右值引用,可以避免不必要的对象拷贝和内存分配,从而提高代码的性能和效率

3.1 移动语义

class Person
{
public:
	//构造函数
	Person(std::string name, int age): m_name(name), m_age(age){
		m_birthgift = new std::string[age];
		for (int i = 0; i < age; i++) {
			std::stringstream ss;
			ss << i+1;
			std::string str = ss.str() + "Year";
			m_birthgift[i] = str;
		}
		std::cout << "Constructor Called" << std::endl;
	};

	//移动构造函数
	Person(Person&& person) noexcept 
		//成员函数接管源对象person的资源
		: m_name(person.m_name), m_birthgift(person.m_birthgift), m_age(person.m_age){
		person.m_name = "";
		person.m_birthgift = nullptr;   // 令源对象person进入这样的状态-对其运行析构函数时安全的。否则源对象析构的时候,会释放刚移动的内存。会导致和移动后对象有效但无定义。
		person.m_age = 0;
		std::cout << "Move Constructor Called" << std::endl;
	};

	Person &operator=(Person &&person) noexcept {
		//直接检查自赋值
		//检查自赋值的原因:万一指向相同对象,左侧资源释放的时候就当于把右侧资源也释放了,此时左侧资源还没有接管右侧资源。
		if (this != &person) {
			//释放移动后对象的已有资源
			delete[] m_birthgift;

			//从源对象接管资源
			m_name = person.m_name;
			m_age = person.m_age;
			m_birthgift = new std::string[m_age];
			for (int i = 0; i < m_age; i++) {
				std::stringstream ss;
				ss << i + 1;
				std::string str = ss.str() + "Year";
				m_birthgift[i] = str;
			}

			// 令源对象person进入这样的状态-对其运行析构函数时安全的。否则源对象析构的时候,会释放刚移动的内存。会导致和移动后对象有效但未定义。
			person.m_birthgift = nullptr;
		}
		return *this;
	}

	//析构函数
	~Person(){
		delete[] m_birthgift;
		std::cout << "Destructor Called" << std::endl;
	};
private:
	std::string m_name;
	std::string* m_birthgift;
	int m_age;
};
int main()
{
	Person p1("Tom", 20);    //创建一个对象
	Person p2(std::move(p1));   //使用移动构造函数将p1移动到p2
	//移动后p1不再指向任何有效的对象,且在移动构造函数中将p1的状态设为无效。不能再对它进行任何操作(访问、修改和销毁)。

	//输出结果
	/*
	Constructor called
	Move constructor called
	Destructor called       //源对象析构
	Destructor called       //移动后对象析构
	*/
	std::cout << "Hello World!\n";
}

3.1 完美转发

完美转发原理是根据参数的值类别来决定如何转发参数。

// 接受参数的函数,转发右值或左值
void otherFunction(int& arg) {
	std::cout << "Received lvalue reference: " << arg << std::endl;
}

void otherFunction(int&& arg) {
	std::cout << "Received rvalue reference: " << arg << std::endl;
}

// 函数模板,使用完美转发传递参数
template<typename T>
void forwardFunction(T&& arg) {
	// 在这里可以对参数进行操作
	std::cout << "Received argument: " << arg << std::endl;
	// 使用 std::forward 将参数完美转发给其他函数
	otherFunction(std::forward<T>(arg));
}

// 接受 const 参数的函数
void otherFunction1(const int& arg) {
	std::cout << "Received const lvalue reference: " << arg << std::endl;
}

// 函数模板,使用完美转发传递 const 参数
template <typename T>
void forwardFunction1(const T& arg) {
	// 在这里可以对参数进行操作
	std::cout << "Received const lvalue reference: " << arg << std::endl;

	// 使用 std::forward 将参数完美转发给其他函数
	otherFunction1(std::forward<const T&>(arg));
}
int main()
{
		/*
		在函数调用中,传递左值和右值作为参数有一些区别:
		1. 传递左值:当将左值传递给函数时,函数参数可以是非常量左值引用(T&)或常量左值引用(const T&)。这允许函数修改左值的值或状态。
		2. 传递右值:当将右值传递给函数时,函数参数可以是非常量右值引用(T&&)或常量右值引用(const T&&)。这允许函数移动或使用右值的值,但不允许修改其值或状态。
		在完美转发的上下文中,使用 std::forward 可以保留参数的值类别,从而实现对左值和右值的正确传递。通过使用 std::forward,可以将左值作为左值引用传递,将右值作为右值引用传递,从而实现最佳性能和语义。
		在示例代码中,forwardFunction 使用完美转发将参数传递给 otherFunction。当传递左值时,T 被推导为左值引用类型,从而将参数作为左值引用传递给 otherFunction。当传递右值时,T 被推导为非常量右值引用类型,从而将参数作为右值引用传递给 otherFunction。
		这样,otherFunction 可以根据参数的值类别进行不同的处理,以实现对左值和右值的正确操作。
		*/
		int value = 42;
		// 传递 lvalue
		forwardFunction(value);
		// 传递 rvalue
		forwardFunction(123);
		
		/*
		在上述示例中,我们定义了一个函数模板 forwardFunction,它接受一个 const 引用参数 arg,并使用完美转发将该参数传递给 otherFunction。
		在 forwardFunction 中,我们使用 std::forward 来保留参数的 const 限定符
		*/
		int value1 = 42;

		// 传递 const lvalue
		forwardFunction1(value1);

		// 传递 const rvalue
		forwardFunction1(123);
	}

    std::cout << "Hello World!\n";
}

四、总结

总结起来,右值引用是 C++11 引入的重要特性,用于提高代码的性能和效率。通过移动语义和完美转发,右值引用可以优化对象的拷贝和内存分配,同时保持代码的简洁性和可读性。在适当的场景下,使用右值引用可以显著改善代码的性能。

你可能感兴趣的:(C++11,c++,开发语言)