Bootstrap

C++中的左值(lvalue)和 右值(rvalue),移动语义(move semantics)和完美转发(perfect forwarding)

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
;