左值(Lvalue)与右值(Rvalue)
英文含义:
-
左值(Lvalue):Locator value,意味着它指向一个具体的内存位置。
-
右值(Rvalue):Read value,指的是可以读取的数据,但不一定指向一个固定的内存位置。
定义
- 左值:指的是一个持久的内存地址。左值可以出现在赋值操作的左侧或右侧。例如,变量、数组的元素、对对象成员的引用等都是左值。
- 右值:通常是临时的、不能有多个引用的值,它们不指向持久的内存地址。右值可以出现在赋值操作的右侧,但不能出现在左侧。字面量(如42、3.14)、临时对象、以及返回临时对象的表达式等都是右值。
完美转发(Perfect Forwarding)
完美转发是C++11引入的一个概念,其目的是允许函数模板将参数以原来的左值或右值的形式转发到其他函数。这是通过引用折叠规则和std::forward
函数实现的。完美转发的一个关键应用场景是模板函数中,我们希望将接收到的参数以完全相同的形式(保持其左值或右值性质)传递给另一个函数时使用。
引用折叠规则
在模板函数或类中,当一个引用的引用被形成时,它们会折叠成单一的引用
T& &
,T& &&
,T&& &
都会被折叠为T&
T&& &&
会被折叠为T&&
示例
- 当
wrapper(lv)
被调用时,lv
是一个左值,因此模板参数T
被推断为int&
(左值引用)。由于引用折叠规则,T&&
折叠为int&
。因此,std::forward<T>(arg)
将arg
作为左值引用转发给process
函数,调用process(int& i)
版本。 - 当
wrapper(20)
被调用时,20
是一个右值,因此模板参数T
被推断为int
。由于T
是一个非引用类型,T&&
就直接是int&&
(右值引用)。因此,std::forward<T>(arg)
将arg
作为右值引用转发给process
函数,调用process(int&& i)
版本。
#include <iostream>
#include <utility> // std::forward
// 分别处理左值和右值
void process(int& i) {
std::cout << "Process left value: " << i << std::endl;
}
void process(int&& i) {
std::cout << "Process right value: " << i << std::endl;
}
// 完美转发的模板函数
template<typename T>
void wrapper(T&& arg) {
// 使用std::forward来完美转发arg
process(std::forward<T>(arg));
}
int main() {
int lv = 10; // 左值
wrapper(lv); // arg被推断为左值引用,因为lv是一个左值
wrapper(20); // 20是右值,arg被推断为右值引用
return 0;
}
/*
Process left value: 10
Process right value: 20
*/
转移(Move)
转移是指将一个对象的资源(如动态内存)从一个实例转移到另一个实例,而不是复制资源。这通常通过移动构造函数和移动赋值操作符实现,它们接受一个右值引用(Rvalue reference)作为参数。移动语义允许资源的高效转移,避免了不必要的复制,特别是对于大型对象或资源密集型对象。
使用
std::move
方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
从实现上讲,
std::move
基本等同于一个类型转换:static_cast<T&&>(lvalue);
,函数原型如下:
template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}
联系
- 完美转发和移动语义都紧密依赖于左值和右值的概念。完美转发用于保持参数的左值或右值性质不变,而移动语义则是利用右值(通常是即将销毁的临时对象)来优化资源的管理。
- 移动语义是完美转发常见的一个应用场景。当使用完美转发将函数参数传递给另一个函数时,如果该参数是一个临时对象(右值),则可以利用移动构造函数或移动赋值操作符,从而提高效率。
- 右值的概念是转移语义的基础。只有右值(临时对象或显式标记为右值的对象)才能被移动,以此来优化资源的使用和提高程序的运行效率。
示例
以下是一个简单示例,其中包含一个自定义的String
类,这个类通过实现移动构造函数和移动赋值操作符来优化内存资源管理。
同时,代码使用完美转发的函数模板,它可以根据传入参数的类型(左值或右值)来决定是否使用移动语义。
#include <iostream>
#include <cstring>
#include <utility> // std::move and std::forward
class String {
private:
char* data;
size_t length;
void freeData() {
delete[] data;
}
public:
// 构造函数
String(const char* p = "") : length(strlen(p)), data(new char[length + 1]) {
std::copy(p, p + length + 1, data);
std::cout << "Constructed\n";
}
// 析构函数
~String() {
freeData();
}
// 拷贝构造函数
String(const String& other) : length(other.length), data(new char[length + 1]) {
std::copy(other.data, other.data + length + 1, data);
std::cout << "Copied\n";
}
// 移动构造函数
String(String&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
std::cout << "Moved\n";
}
// 移动语义的赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
freeData();
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
std::cout << "Move Assigned\n";
}
return *this;
}
void print() const {
if (data) {
std::cout << data << std::endl;
}
}
};
// 完美转发示例
template<typename T>
void relay(T&& arg) {
// 使用 完全转发 来保持'arg'的左值/右值性质。
String temp(std::forward<T>(arg));
temp.print();
}
int main() {
String s1("Hello");
String s2(std::move(s1)); // 调用移动构造函数
s1 = String("World"); // 移动语义赋值调用
String s3("Goodbye");
relay(s3); // 左值被传递
relay(String("Hello World")); // 右值被传递
return 0;
}
/*
Constructed
Moved
Constructed
Move Assigned
Constructed
Copied
Goodbye
Constructed
Moved
Hello World
*/
程序输出:
Constructed
Moved
Constructed
Move Assigned
Constructed
Copied
Goodbye
Constructed
Moved
Hello World
String
类包含了移动构造函数和移动赋值操作符,当与右值交互时,可以有效地转移资源而不是进行复制。这样,当有一个临时的String
对象时(例如在main
函数中通过String("World")
创建的临时对象),这个对象的资源可以被转移到另一个对象中而不需要额外的复制开销。
参考
-
爱编程的大丙:转移和完美转发
-
爱编程的大丙:右值引用
评论区