C++左值右值与右值引用

tech2026-10-06  0

本文总结 C++ 左右值的定义,和 C++11 中右值引用的用途。

转载:C++11新特性3 - 左右值与右值引用 C++ 11: Rvalue Reference – Move Sematics C++ 11: Rvalue Reference – Fowarding


C++11通过引入右值引用来优化性能,通过移动语义来减少无谓的拷贝,通过完美转发来解决不能按照参数实际类型来

要点:

什么是右值:字面值和临时值/将亡值称为右值,或者说不能取地址的值称为右值右值引用特点一:引用函数返回的将亡值,延长将亡值得生命周期,减少对象拷贝次数(const常量引用也能实现同样的效果,编译器返回值优化RVO能实现更好的效果(g++ -fno-elide-constructors关闭优化))右值引用特点二:既可引用右值,又可引用左值,右值引用特点三:T&& t是未定的引用类型(通用引用),是左值还是右值取决于它的初始化,可实现完美转发

1. 左值与右值

C++ 中,左右值的简化定义:

左值:占用了一定内存,且拥有可辨认的地址的对象右值:左值以外的所有对象 典型的左值,C++中绝大部分的变量都是左值 int i = 1; // i 是左值 int *p = &i; // i 的地址是可辨认的 i = 2; // i 在内存中的值可以改变 class A; A a; // 用户自定义类实例化的对象也是左值

典型的右值

int i = 2; // 2 是右值 int x = i + 2; // (i + 2) 是右值 int *p = &(i + 2);//错误,不能对右值取地址 i + 2 = 4; // 错误,不能对右值赋值 class A; A a = A(); // A() 是右值 int sum(int a, int b) {return a + b;} int i = sum(1, 2) // sum(1, 2) 是右值

引用(左值引用)

int i; int &r = i; int &r = 5; // 错误,不能左值引用绑定右值 // const 引用是例外 // 可以认为指向一个临时左值的引用,其值为5 const int &r = 5;

对于函数,类似地有

int square(int& a) {return a * a;} int i = 2; square(i); // 正确 square(2); // 报错,左值引用绑定右值 // 变通一下 int square(const int& a) { return a * a; } // 可以调用 square(2)

左值可以创造右值, 右值也可创造左值

int i = 2; int x = i + 2; // i + 2 是右值 int v[3]; // 指针解引用可以把右值转化为左值 *(v + 1) = 2;

一些注意事项

// 1. 函数也可以返回左值 int myglobal ; int& foo() {return myglobal;} foo() = 50; // 一个常见的例子,[]操作符几乎总是返回左值 array[3] = 50; // 2. 左值并不总是可修改的 // i是左值, 但不可修改 const int i = 1; // 3. 右值有时是可修改的 class dog; // bark() 可能会修改 dog() 对象 dog().bark();

2. 右值引用

右值引用的两大用途:

延长临时值生命周期移动构造,浅拷贝完美转发,区分左值右值

2.1 右值引用临时值

以下代码调用了几次A的构造函数:

A getA() { return A(); } int main() { // 这句代码调用了几次A的构造函数(1次无参构造,2次拷贝构造) A a1 = getA(); // 这句代码调用了几次A的构造函数(1次无参构造,1次拷贝构造) const A& a2 = getA(); // 这句代码调用了几次A的构造函数(1次无参构造,1次拷贝构造) A& a3 = getA(); }

右值引用能延长临时变量的声明周期,起到和常量左值引用类似的效果;但是常量左值引用是个常量,而右值引用不是常量,可以调用对象的const和非const函数。

2.2 移动语意与移动构造

类通常都会带有一个拷贝构造函数,但有时候我们又需要一个浅拷贝和一个深拷贝两个构造函数。尤其是拷贝构造的参数是个临时值时,我们通常希望只做浅拷贝,把成员变量的所有权移动至新对象,这就是所谓的移动语义,右值引用的一个重要作用就是用来支持移动语义。

如果从临时对象拷贝构造,希望调用浅拷贝;如果从右值对象拷贝构造,希望调用深拷贝:

class A { public: A() { } A(const A&a) // 拷贝构造 { size_t len = strlen(a->str); str = (char*)malloc(len+1); memset(str,0,len+1); memcpy(str,a->str,len); } A(A&&a) // 移动构造 { str = a->str; a->str = NULL; } ~A() { if(nullptr != str) { free(str); str = NULL; } } char * str =NULL; } A GetA() { return A(); } int main() { A a = GetA(); // 调用移动构造 A a2 = a; // 调用拷贝构造 }

2.2.1 std::move右值引用类型转换

在上面的移动构造函数中,参数必须是右值引用,如果一个左值也想要使用移动构造函数创建新的对象,该怎么办?

移动语意(std::move),可以将左值转化为右值引用。查看源码,std::move等同于一个类型转换:static_cast<T&&>(lvalue)。

还是使用上面的代码测试:

int main() { A a = GetA(); // 调用移动构造 A a2 = a; // 调用拷贝构造 A a3 = std::move(a); // 调用移动构造,1. std::move(a) 返回a的右值引用;2. 以a的右值引用移动构造a3 }

其他测试:

int a = 1; // 左值 int &b = a; // 左值引用 // 移动语意: 转换左值为右值引用 int &&c = std::move(a); void printInt(int& i) { cout << "lval ref: " << i << endl; } void printInt(int&& i) { cout << "rval ref: " << i << endl; } int main() { int i = 1; // 调用 printInt(int&), i是左值 printInt(i); // 调用 printInt(int&&), 6是右值 printInt(6); // 调用 printInt(int&&),移动语意 printInt(std::move(i)); }

由于编译器调用时无法区分

printInt(int) 与 printInt(int&)printInt(int) 与 printInt(int&&)

如果再定义 printInt(int) 函数,会报错

为什么需要移动语意

class myVector { int size; double* array; public: // 复制构造函数 myVector(const myVector& rhs) { std::cout << "Copy Construct\n"; size = rhs.size; array = new double[size]; for (int i=0; i<size; i++) { array[i] = rhs.array[i]; } } myVector(int n) { size = n; array = new double[n]; } }; void foo(myVector v) { /* Do something */ } // 假设有一个函数,返回值是一个 MyVector myVector createMyVector(); int main() { // Case 1: myVector reusable=createMyVector(); // 这里会调用 myVector 的复制构造函数 // 如果我们不希望 foo 随意修改 reusable // 这样做是 ok 的 foo(reusable); /* Do something with reusable */ // Case 2: // createMyVector 会返回一个临时的右值 // 传参过程中会调用拷贝构造函数 // 多余地被复制一次 // 虽然大部分情况下会被编译器优化掉 foo(createMyVector()); }

解决方法, 添加一个移动构造函数

// 移动构造函数 myVector(myVector&& rhs) { std::cout << "Move Constructor\n"; size = rhs.size; array = rhs.array; rhs.size = 0; rhs.array = nullptr; }

那么,foo(createMyVector())就不会调用拷贝构造函数,而会调用移动构造函数 (当然更大的可能性是编译器直接把这一步优化掉)

然而,在 C++03 中,为了解决这个问题,可能需要定义两个 foo 函数,比较麻烦:

foo_by_value(myVector)foo_by_ref(myVector&)

同时,假如我们在调用 foo 之后,reusable 就不再被使用了, 可以使用移动语意

int main () { myVector reusable = createMyVector(); // 这里会调用 myVector 的移动构造函数 foo(std::move(reusable)); /* No use of reusable anymore */ }

另一个有用之处在于重载赋值运算符

X& X::operator=(X const & rhs); X& X::operator=(X&& rhs);

自从 C++11,所有的 STL 都实现了移动构造

2.3 完美转发与引用折叠

在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。完美转发需要用到std::forward函数。

完美转发是指将右值引用实际引用的值类型进行转发,左值转发到左值引用的函数,右值转发到右值引用的函数。可以发现移动构造也是通过完美转发实现的。

左值被转发为左值,右值被转发为右值 void show_log(string& str) { cout << "string & show " << str.data() << endl; } void show_log(string &&str) { cout << "string && show " << str.data() << endl; } // 参数转发 template <typename T> void show(T&& arg) { // show_log(arg); // arg为具名变量,为左值,只有左值函数会被调用 show_log(std::forward<T>(arg)); // std::forward获取变量的原类型 } int main() { string msg = "hello"; show(msg); show(std::move(msg)); } void foo(myVector& v) {} // 参数转发 template<typename T> void relay(T arg) { foo(arg); } int main() { myVector reusable= reateMyVector(); // 拷贝构造函数 relay(reusable); // 移动构造函数 relay(createMyVector()); }

这个实现有个问题,假如定义了两个版本的 foo

void foo(myVector& v) {} void foo(myVector&& v) {}

永远只有 foo(myVector& v) 会被调用 (右值引用myVector&& v是个左值,这里的v是有名字的)

所以,我们需要改写上文的 relay 函数,借助 std::forward

template<typename T> void relay(T&& arg) { foo(std::forward<T>(arg)); }

于是就有

relay(reusable) 调用 foo(myVector&)relay(createMyVector()) 调用 foo(myVector&&)

要解释完美转发的原理,首先引入 C++11 的引用折叠原则

T& & => T&T& && => T&T&& & => T&T&& && => T&&

所以,在 relay 函数中

relay(9); => T = int&& => T&& = int&& && = int&&relay(x); => T = int& => T&& = int& && = int &

因此,在这种情况下,T&& 被称作 universal reference,即满足

T 是模板类型T 是被推导出来的,适用引用折叠,即 T 是一个函数模板类型,而不是类模板类型

然后,C++11 定义了 remove_reference,用于返回引用指向的类型

template<class T> struct remove_reference; remove_reference<int&>::type == int remove_reference<int>::type == int

于是,std::forword 的实现如下

template<class T> T&& forward( typename remove_reference<T>::type& arg ) { return static_cast<T&&>(arg); }

等于是把右值引用(右值引用本身是个左值)转成了右值,左值保持不变

3. 小结

std::move 对比 std::forward:

std::move(arg) 将 arg 转化为右值std::forward(arg) 将 arg 转化为 T&&
最新回复(0)