在C++中,左值(lvalue
)和右值(rvalue
)是表达式的两种基本分类,它们决定了表达式的结果在内存中的位置和状态。左值通常指的是具有持久状态的对象,它们有明确的内存地址,可以被多次赋值。而右值通常是临时的、没有持久状态的值,它们通常没有内存地址,或者其内存地址在表达式结束后就变得无效。
C++11引入了右值引用(rvalue reference
),用T&&
表示,作为对左值引用(lvalue reference
,用T&
表示)的补充。这一特性极大地增强了C++的表达能力,特别是在资源管理和性能方面。
(1)左值引用
左值引用是C++98就有的特性,它允许我们为已存在的对象创建一个别名。左值引用必须被初始化为一个左值,即一个具有持久状态的对象。
int a = 10;
int& b = a; // b是a的左值引用
(2)右值引用
右值引用是C++11新增的特性,它允许我们为右值(即临时对象或即将被销毁的对象)创建一个引用。这样,我们就可以对右值进行更复杂的操作,比如移动语义(move semantics)。
int&& c = 20; // c是整数字面量20的右值引用(但这种情况不常见,通常用于函数参数或返回值)
std::string foo() {
return std::string("Hello, World!"); // 返回的临时字符串是一个右值
}
std::string &&d = foo(); // d是foo()返回的临时字符串的右值引用
但请注意,直接绑定一个右值到右值引用(如int&& c = 20;
)并不是右值引用的主要用途。右值引用的主要用途是作为函数参数(实现移动语义)和返回值(允许链式调用等)。
(3)move函数
std::move
的作用:
-
将左值转换为右值,以触发移动语义。
-
不会真正“移动”数据,只是改变对象的属性。
-
用于触发移动构造和移动赋值,避免深拷贝,提高性能。
std::move
的底层原理
-
std::move()
只是static_cast<T&&>
,不会改变对象的生命周期。 -
std::move(a)
只是告诉编译器a
变成右值,但不会修改a
本身。
#include<vector>
class Vector
{
private:
int x, y, z;
public:
Vector(int x, int y, int z) :x(x), y(y), z(z) {}
};
int main()
{
std::vector<Vector> vec;
vec.push_back(Vector(1,2,3));//2
Vector a(4,5,6);
vec.push_back(a);//1
vec.push_back(std::move(a));//2
}
我们来看一下这段代码,第一个push_back里是一个临时变量还记得吗?临时变量都是右值,第二个push_back,因为a是个左值所以传入的参数是个左值,第三个push_back我们使用了move方法本质上我们希望他变成一个右值进而发生移动语义,就是一个偷的过程,而不是复制的过程,让我们进到源码里看看是什么情况.要记住move 它不进行任何移动.还要知道一件事:
在运行期move是无作为的.
_CONSTEXPR20_CONTAINER void push_back(const _Ty& _Val) { // insert element at end, provide strong guarantee
emplace_back(_Val);
}
_CONSTEXPR20_CONTAINER void push_back(_Ty&& _Val) {
// insert by moving into element at end, provide strong guarantee
emplace_back(_STD move(_Val));
}
补充:
c++14 新增 remove_reference_t (引用移除)
std::remove_reference
其中:
std::remove_reference_t 实现:
template< class T >
using remove_reference_t = typename remove_reference<T>::type;
样例:
#include <iostream> // std::cout
#include <type_traits> // std::is_same
template<class T1, class T2>
void print_is_same() {
std::cout << std::is_same<T1, T2>() << '\n';
}
int main() {
std::cout << std::boolalpha;
print_is_same<int, int>();
print_is_same<int, int &>();
print_is_same<int, int &&>();
print_is_same<int, std::remove_reference<int>::type>();
print_is_same<int, std::remove_reference<int &>::type>();
print_is_same<int, std::remove_reference<int &&>::type>();
}
输出:
true
false
false
true
true
true
我们看到了一个push_back的重载我们通过调试可以得知,第一个push_back调用的是源码的第二个,第二个push_back调用的是源码的第一个,第三个调用的是第二个(偷懒一下),要注意的是(_Ty&& _Val)它并不是一个万能引用,因为vector是一个类模板,(之后我会出博客讲到万能引用和引用叠加等等...)这里的TY就是type的意思就是参数的类型,会进行模板推导.第一个push_back的参数是一个左值引用的形式,第二个是右值引用的形式,第二个会触发一个移动语义,将原先的a的内存偷了过来。
为了加深理解,我们看一下move的源码并且拿过来将代码变为下面这样,变成我们自己的move看看是否能运行成功。
#include<vector>
#include <type_traits>
// FUNCTION TEMPLATE move
template <class _Ty>
constexpr std::remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<std::remove_reference_t<_Ty>&&>(_Arg);
}
class Vector
{
private:
int x, y, z;
public:
Vector(int x, int y, int z) :x(x), y(y), z(z) {}
};
int main()
{
std::vector<Vector> vec;
vec.push_back(Vector(1,2,3));
Vector a(4,5,6);
vec.push_back(a);
vec.push_back(move(a));
}
我们可以看到我们将move搬过来实现一样可以运行成功,我们来看源码,_t是C++14之后将原来的type的形式全部都变成type reference的形式,remove_reference_t就是将这个函数木板的类别<_Ty>它的加引用的情况都给去掉了,无论是左值引用(&)还是右值引用(&&)都会移除掉,之后再用static_cast强转为右值引用的形式,那么我们能看出move就是将参数原来的修饰符全部都删掉,在强转为右值引用输出,就是这么简单,move没有干任何移动的过程,所以还是那句话:
std::move 并不会进行任何移动
真正的移动是要自己写的,发生在之后也就是这里
public:
template <class... _Valty>
_CONSTEXPR20_CONTAINER decltype(auto) emplace_back(_Valty&&... _Val) {
// insert by perfectly forwarding into element at end, provide strong guarantee
auto& _My_data = _Mypair._Myval2;
pointer& _Mylast = _My_data._Mylast;
if (_Mylast != _My_data._Myend) {
return _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
}
_Ty& _Result = *_Emplace_reallocate(_Mylast, _STD forward<_Valty>(_Val)...);
#if _HAS_CXX17
return _Result;
#else // ^^^ _HAS_CXX17 ^^^ // vvv !_HAS_CXX17 vvv
(void) _Result;
#endif // _HAS_CXX17
}
然而move的作用也就是强行转换成右值引用。
(4)移动语义(Move Semantics)
移动语义是C++11引入的一个概念,它允许资源的所有权从一个对象“移动”到另一个对象,而不是通过拷贝。移动语义通过右值引用和移动构造函数来实现,从而提高了程序的性能。
4.1 为什么需要移动语义?
当我们处理大型对象(如 std::vector
或 std::string
等)时,拷贝操作通常非常昂贵,因为这些对象可能会涉及大量的动态内存分配和数据复制。移动语义允许我们在这些对象之间转移所有权,而不是进行复制,从而节省时间和资源。
4.2 如何实现移动语义?
C++中的移动语义是通过右值引用和移动构造函数来实现的。右值引用允许我们识别临时对象,而移动构造函数则允许将对象的资源(如内存)从一个对象转移到另一个对象,而不需要拷贝数据。
移动构造函数的示例:
#include <iostream>
#include <vector>
class MyClass {
public:
std::vector<int> data;
MyClass() {
std::cout << "Default constructor" << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept {
std::cout << "Move constructor" << std::endl;
data = std::move(other.data); // 通过std::move转移资源 必须显示使用std::move,否则就是触发拷贝构造函数
//data = other.data// 不行,会触发拷贝构造函数而不是移动构造函数
}
};
int main() {
MyClass obj1; // 默认构造
MyClass obj2 = std::move(obj1); // 移动构造,obj1的资源被转移到obj2
}
解释:
MyClass(MyClass&& other)
是一个移动构造函数,它通过std::move
将other
对象的资源(data
)转移给新的对象。std::move(other.data)
并不会进行拷贝,而是“转移”资源。
在上面的代码中:
obj1
是原始对象,obj2
是通过移动构造从obj1
创建的新对象。- 移动构造不会拷贝数据,而是将
obj1
的内部数据转移到obj2
,同时确保obj1
不再拥有这些资源。
(5)完美转发(Perfect Forwarding)
在 C++11 之前,泛型函数在传递参数时无法保持参数的原始类型(左值或右值),导致额外的拷贝或移动操作。完美转发(Perfect Forwarding)是一种 高效传递参数 的技术,能够 保持参数的原始特性,避免额外的性能开销。
5.1 什么是完美转发?
完美转发 是指 在泛型模板函数中,以参数的原始形式(左值或右值)传递给目标函数,从而避免 不必要的拷贝或移动操作。
通过完美转发,我们确保传递给另一个函数的参数类型和价值(左值或右值)保持一致。
5.2 如何实现完美转发?
完美转发依赖于 右值引用 和 std::forward
。std::forward
是一个特殊的函数模板,它在传递参数时能够保留其原始的类型(左值或右值)。
示例代码:
#include < iostream>
using namespace std;
void process(int &x){cout << "Lvalue reference: " << x << endl;}
void process(int &&x) {cout << "Rvalue reference: " << x << endl; }
// 泛型函数,使用完美转发
template <typename T>
void forwardExample(T&& arg)
{
process(std::forward<T>(arg)); // 关键:std::forward 保持原始类型
}
int main()
{
int a = 10;
forwardExample(a);// 传递左值
forwardExample(20); // 传递右值
return 0;
}
//输出
Lvalue reference: 10
Rvalue reference: 20
分析:
-
std::forward<T>(arg)
让arg
保持左值或右值特性,从而 正确调用process(int&)
或process(int&&)
。 -
forwardExample(a)
传递左值,std::forward<T>(a)
仍是左值。 -
forwardExample(20)
传递右值,std::forward<T>(20)
仍是右值。
5.3 如果去掉 std::forward
会怎样?
template <typename T>
void forwardExample(T&& arg)
{
process(arg); // 没有 std::forward
}
//输出
Lvalue reference: 10
Lvalue reference: 20 // 右值变成了左值!
错误分析:
-
arg
在process(arg)
语境中变成了左值,即使forwardExample(20)
传递的是右值,arg
也会 丢失右值特性。 -
结果:
process(int&&)
无法调用,所有右值都会被当成左值,导致 额外拷贝或移动。
5.4 std::forward
的工作原理
std::forward<T>(arg)
通过 引用折叠(Reference Collapsing)和 类型推导 来决定参数是否应该保留右值特性。
传入实参 | T 推导结果 | 函数参数类型 (T&& ) | 引用折叠后类型 | std::forward<T>(arg) 的类型 | 值类别 |
---|---|---|---|---|---|
int x (左值) | int& | int& && | int& | int& | 左值 |
const int x (左值) | const int& | const int& && | const int& | const int& | 左值 |
20 (右值) | int | int&& | int&& | int&& | 右值 |
std::move(x) | int | int&& | int&& | int&& | 右值 |
T&& val = 左值引用实参(手动调用 wrapper) | int& | int& && | int& | int& | 左值 |
T&& val = 右值引用实参(手动调用 wrapper) | int&& | int&& && | int&& | int&& | 右值 |
核心规则:
-
T&&
绑定左值 时,T
会被推导为int&
,最终std::forward<int&>(arg)
仍是左值。 -
T&&
绑定右值 时,T
会被推导为int
,最终std::forward<int>(arg)
仍是右值。
5.5 完美转发的应用场景
(1)传递构造函数参数
class Myclass {
public:
template <typename T>
Myclass(T&& arg) : data(std::forward<T>(arg)) {)
private:
int data;
};
std::forward<T>(arg)
确保 arg
以最佳方式传递给 data
,避免不必要的拷贝。
(2)传递函数参数
#include <iostream>
#include <string>
#include <utility>
void print(const std::string& s)
{
std::cout << "Lvalue: " << s << std::endl;
}
void print(std::string&& s)
{
std::cout << "Rvalue: " << s << std::endl;
}
// 通过完美转发调用 print
template <typename T>
void callPrint(T&& arg)
{
print(std::forward<T>(arg)); // ✅ 注意是 std::forward
}
int main() {
std::string str = "Hello";
callPrint(str); // 传左值 → 调用 Lvalue 版本
callPrint("World"); // 传右值 → 调用 Rvalue 版本
}