系列文章目录
文章目录
前言
开发中很多情况会发生对象拷贝,在某些情况下,对象拷贝后立即就被销毁了,在这些情况下,从旧内存将元素拷贝到新内存是不必要的。而且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,以提供额外的异常安全性保证。这可以帮助确保在移动操作失败时,对象仍然处于有效且可用的状态,提高程序的可靠性和健壮性。
- 由于移动操作是窃取资源,它通常不分配任何资源。因此移动操作通常不会抛出任何异常。
- 当编写一个不抛出异常的的移动操作时,我们应该将此时通知标准库。否则标准库会认为移动我们的类对象可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。
- 当移动构造函数声明为 noexcept 时,它表示移动操作不会抛出异常。这可以提供额外的异常安全性保证,称为强异常安全保证(strong exception safety
guarantee)。
强异常安全保证要求在移动构造函数中,如果发生异常,对象的状态不会发生改变,即要么移动操作成功完成,要么对象保持在移动之前的状态。这可以确保在移动操作失败时,对象仍然处于有效且可用的状态,不会导致资源泄漏或不一致的状态。- 如果移动构造函数没有声明为 noexcept,则无法提供强异常安全保证。在移动操作中,如果移动构造函数抛出异常,对象可能会处于部分移动的状态,资源可能会泄漏或对象可能会处于不一致的状态。
2.2.3 使移动源对象进入是可析构状态
通过将源对象的指针置为nullptr来实现源对象进入一个可析构状态。
对象移动并不会销毁此对象,但有时在移动操作后,源对象会被销毁。因此我们必须确保源对象进入一个可析构状态。这样即使源对象被析构,也不会释放对象内存。
三、使用场景
右值引用在以下情况下特别有用:
- 移动语义:当需要转移资源所有权而不进行深拷贝时,可以使用右值引用来实现高效的移动语义。
- 完美转发:当需要将参数以原样传递给其他函数时(同时保留其值类别(左值或右值))和常量性(const限定符),可以使用右值引用来实现完美转发,可以避免不必要的拷贝和类型转换。在泛型编程中特别有用。
- 优化性能:通过使用右值引用,可以避免不必要的对象拷贝和内存分配,从而提高代码的性能和效率
3.1 移动语义
cpp
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;
};
cpp
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 完美转发
完美转发原理是根据参数的值类别来决定如何转发参数。
cpp
// 接受参数的函数,转发右值或左值
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));
}
cpp
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 引入的重要特性,用于提高代码的性能和效率。通过移动语义和完美转发,右值引用可以优化对象的拷贝和内存分配,同时保持代码的简洁性和可读性。在适当的场景下,使用右值引用可以显著改善代码的性能。