C++中的左值(lvalue)和 右值(rvalue),移动语义(move semantics)和完美转发(perfect forwarding)
flyfish
在C++中,表达式可以是左值(lvalue)或右值(rvalue)。左值和右值的区别主要在于它们的值类别(value category)和它们在表达式中的使用方式。
左值(lvalue)
左值表示一个对象或内存位置,可以被赋值或取地址。左值通常是具名变量、数组元素或函数调用返回的左值引用。
右值(rvalue)
右值表示一个临时值或字面常量,不能被赋值,但可以用于初始化另一个对象或被移动。
左值示例
在这个示例中,x 和 y 都是左值,因为它们表示内存中的一个对象,可以被赋值和取地址。
#include <iostream>
int main() {
int x = 10; // 变量 x 是左值,可以被赋值
int y = x; // 变量 x 是左值,可以用于赋值
int* p = &x; // 变量 x 是左值,可以取地址
std::cout << "x: " << x << std::endl;
std::cout << "y: " << y << std::endl;
std::cout << "Address of x: " << p << std::endl;
return 0;
}
x: 10
y: 10
Address of x: 0000008C979DF694
右值示例
在这个示例中,表达式 x + 5 和字面常量 42 都是右值,因为它们表示临时值,不可以取地址。
#include <iostream>
int main() {
int x = 10;
int y = x + 5; // 表达式 x + 5 是右值,表示一个临时值, x + 5 不可以取地址
int z = 42; // 字面常量 42 是右值,表示一个临时值, 42不可以取地址
std::cout << "x: " << x << std::endl;
std::cout << "y: " << y << std::endl;
std::cout << "z: " << z << std::endl;
return 0;
}
左值引用和右值引用
printLvalue 函数接受左值引用参数,只能传递左值。
printRvalue 函数接受右值引用参数,只能传递右值。
调用 printRvalue(20) 和 printRvalue(a + 5) 都是传递右值,而调用 printLvalue(a) 是传递左值。
#include <iostream>
// 左值引用参数
void printLvalue(int& x) {
std::cout << "Lvalue: " << x << std::endl;
}
// 右值引用参数
void printRvalue(int&& x) {
std::cout << "Rvalue: " << x << std::endl;
}
int main() {
int a = 10;
printLvalue(a); // 传递左值
printRvalue(20); // 传递右值
printRvalue(a + 5); // 传递右值
printRvalue(std::move(a)); // 将左值转换为右值
return 0;
}
输出
Lvalue: 10
Rvalue: 20
Rvalue: 15
Rvalue: 10
std::move
std::move 是 C++11 引入的一个标准库函数,用于将对象显式地转换为右值引用(rvalue reference)。它的主要作用是启用对象的移动语义,从而避免昂贵的拷贝操作,提升程序的性能。
std::move 的工作原理
std::move 本质上不会移动任何东西,它只是将其参数转换为右值引用,使其可以被移动构造函数或移动赋值运算符处理。这样可以避免深拷贝,提高效率。
下面的例子std::move 将 vec1 转换为右值引用,允许 vec2 通过移动构造函数获取 vec1 的内容。移动之后,vec1 的大小变为 0,因为其资源已被转移给 vec2。
#include <iostream>
#include <utility> // std::move
#include <vector>
int main() {
std::vector<int> vec1 = {1, 2, 3, 4, 5};
std::vector<int> vec2;
// 使用 std::move 将 vec1 转换为右值引用,vec2 移动构造 vec1 的内容
vec2 = std::move(vec1);
std::cout << "vec1 size: " << vec1.size() << std::endl; // 输出:vec1 size: 0
std::cout << "vec2 size: " << vec2.size() << std::endl; // 输出:vec2 size: 5
return 0;
}
输出
vec1 size: 0
vec2 size: 5
移动语义(Move Semantics)
移动语义是C++11引入的特性,用于优化对象的复制操作,避免不必要的深拷贝,提高程序的性能。它通过使用右值引用(&&)实现对象的移动,而不是复制。
下面是一个带有移动构造函数和移动赋值运算符的自定义类示例
MyClass 有一个带动态内存分配的构造函数。
移动构造函数将资源从源对象转移到目标对象,并将源对象的指针置空。
移动赋值运算符类似,将资源从源对象转移到目标对象,并释放目标对象的原有资源。
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(int size) : size(size), data(new int[size]) {
std::cout << "Constructor called\n";
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
std::cout << "Move constructor called\n";
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
std::cout << "Move assignment operator called\n";
}
return *this;
}
~MyClass() {
delete[] data;
std::cout << "Destructor called\n";
}
private:
int size;
int* data;
};
int main() {
MyClass obj1(10);
MyClass obj2 = std::move(obj1); // 调用移动构造函数
MyClass obj3(20);
obj3 = std::move(obj2); // 调用移动赋值运算符
return 0;
}
输出
Constructor called
Move constructor called
Constructor called
Move assignment operator called
Destructor called
Destructor called
Destructor called
完美转发(Perfect Forwarding)
完美转发允许函数模板将其参数完美地传递给另一个函数,而不改变参数的值类别(左值或右值)。这通常通过使用 std::forward 实现。
完美转发示例
wrapper 函数模板使用 std::forward 将参数完美转发给 process 函数。
process 函数有两个重载版本,一个接受左值引用,另一个接受右值引用。
wrapper 函数根据传入参数的值类别,正确调用对应的 process 函数。
#include <iostream>
#include <utility> // std::forward
// 泛型函数,用于完美转发参数
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
// 被调用的函数
void process(int& x) {
std::cout << "Processing lvalue: " << x << std::endl;
}
void process(int&& x) {
std::cout << "Processing rvalue: " << x << std::endl;
}
int main() {
int a = 5;
wrapper(a); // 传递左值,调用 void process(int& x)
wrapper(10); // 传递右值,调用 void process(int&& x)
wrapper(std::move(a)); // 传递移动的左值,调用 void process(int&& x)
return 0;
}
输出
Processing lvalue: 5
Processing rvalue: 10
Processing rvalue: 5