性能大杀器:std::move 和 std::forward

科技   2024-05-16 08:16   浙江  

今天我们聊聊Modern cpp的两个非常重要的概念移动语义转发引用

概念

值类别

在C++11之前,值类别分为左值和右值两种,但是自C++11起,引入了纯右值,消亡值两种。其中,左值和将亡值合称为泛左值,纯右值和将亡值合称为右值(C++11之前的右值等同于C++11中的纯右值)。因为本文目的不在于分析值类别,所以本文意义中的左值和右值就是字面意义上的左值右值。

右值(RVALUE),即立即创建和使用的临时值。在C++中,右值是与内存地址无关的表达式,这意味着其没有地址,也不能被修改。通常3、1.0以及std::string("abc")这种都属于右值。

PS:需要注意的是常量字符串"abc"等这种属于左值。

与右值相反,左值(LVALUE),其具有内存地址和可修改,其可以用于分配新值或者获取对象的地址。

可能有人有疑问,就是如何区分左值和右值,目前一个比较通用的判断方式就是:判断其是否可以取地址

左值引用 & 右值引用

既然有左值和右值,那么相应的,也就存在左值引用右值引用,常常如下这种表示:

int a = 0;
int &la = a;
int &&r = 3;

在上述示例中,a、la以及r都属于左值,其中la是左值引用,r是右值引用。

看下面一个例子:

#include <iostream>

void Print(int& lref) {
    std::cout << "Lvalue reference" << std::endl;
}

void Print(const int& lref) {
    std::cout << "const Lvalue reference" << std::endl;
}

void Print(int&& rref) {
    std::cout << "Rvalue reference" << std::endl;
}


int main() {
    int x = 5;
    const int y = 10;

    Print(x);   // lvalue reference
    Print(y);   // lvalue reference
    Print(20);  // rvalue reference

    return 0;
}

上述示例输出如下:

Lvalue reference
const Lvalue reference
Rvalue reference

std::move

std::move是C++中的一个常用函数,它执行到右值引用的转换,允许您将左值转换为右值。这在您想要转移所有权或启用对象的移动语义的情况下非常有用。移动语义允许开发人员有效地将资源(如内存或文件句柄)从一个对象传输到另一个对象,而无需进行不必要的复制。

正如字面意义所理解的,移动语义允许将对象有效地从一个位置“移动”到另一个位置,而不是复制,这对于管理资源的对象特别有用。它实际上并没有移动任何东西;它只是将表达式的类型更改为右值引用。这允许调用移动构造函数或移动赋值运算符,而不是调用复制构造函数或复制赋值运算符。

gcc对move的实现如下:

template<typename _Tp>
     inline typename std::remove_reference<_Tp>::type&&
     move(_Tp&& __t)
     { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

也就是说,其仅仅通过static_cast<>做了类型转换~

std::move仅仅将对象转换为右值引用,仅此而已

#include <iostream>
#include <utility>

class Obj {
public:
    Obj() {
        std::cout << "Default constructor\n";
    }

    Obj(const Obj&) {
        std::cout << "Copy constructor\n";
    }

    Obj(Obj&&) noexcept {
        std::cout << "Move constructor\n";
    }

    Obj& operator=(Obj&& other) noexcept {
        std::cout << "Move assignment operator\n";
        
        return *this;
    }

};

int main() {
    Obj obj1;                   /* Default constructor */
    Obj obj2 = std::move(obj1); /* Move constructor    */
    Obj obj3;
    obj3 = std::move(obj2);          /* Move assignment operator */
    return 0;
}

输出如下:

Default constructor
Move constructor
Default constructor
Move assignment operator

在上述示例中:

Obj1创建对象并调用构造函数obj2是通过使用std::move移动obj1创建的,它调用移动构造函数创建obj3并调用默认构造函数当使用std::move将obj2移动到 obj3 时,将调用移动赋值运算符

在此示例中,使用std::move操作, obj1到obj2 以及 obj2到obj3调用的是移动的行为,这样可以提高性能,尤其是在移动大型数据结构或资源时。但是,重要的是要注意移动对象的状态及其拥有的资源。

#include <iostream>
#include <memory>

class Obj {
public:
    Obj() {
        std::cout << "Obj constructed" << std::endl;
    }

    ~Obj() {
        std::cout << "Obj destructed" << std::endl;
    }

    void fun() {
        std::cout << "in fun" << std::endl;
    }
};

int main() {
    std::unique_ptr<Obj> p1 = std::make_unique<Obj>();

    std::unique_ptr<Obj> p2 = std::move(p1);
    if (p1) {
        std::cout << "p1 is not empty" << std::endl;
    }

    p2->fun();

    return 0;
}

在这个例子中,首先创建了一个类型为std::unique_ptr的指针p1,然后通过调用std::move()将p1的所有权转移至p2,接着判断p1是否为有效的指针,如果是则输出,接着p2调用fun()函数。

上述示例输出结果如下:

Obj constructed
in fun
Obj destructed

从这个输出结果可以看出,通过std::move()将所有权从p1转移至p2后,p1不再持有任何资源。

std::forward

std::forward 是 C++ 标准库中的一个函数模板,用于在模板函数中进行完美转发。它允许在模板函数中将参数转发到另一个函数,同时保持参数的值类别(value category)和 cv 限定符(const 或 volatile 限定符)不变。

std::forward 通常与右值引用(&&)结合使用,用于转发传递给模板函数的参数。在模板函数内部,你可以使用 std::forward 来将参数转发给其他函数,并保持原始参数的性质。

示例如下:

#include <iostream>

void Print(const int& lref) {
    std::cout << "Lvalue reference" << std::endl;
}

void Print(int&& rref) {
    std::cout << "Rvalue reference" << std::endl;
}

template<typename T>
void Fun(T&& param) {
    Print(std::forward<T>(param));
}

int main() {
    int x = 5;
    const int y = 10;

    Fun(x);   // lvalue reference
    Fun(y);   // lvalue reference
    Fun(20);  // rvalue reference

    return 0;
}

在这个例子中,我们创建了一个模板函数Fun(),其参数类型为T&&,当使用左值调用Fun()时候,它将param作为左值进行转发,当使用右值调用Fun()时候,它将param作为右值进行转发,然后调用对应的函数,这样可保证在不损失真实类型的情况下调用正确的函数。

move vs forward

对于第一次接触这块知识点的开发人员来说,可能有点疑惑,是否可以用move来替代forward,我们且看一个例子,相信你看了之后就不会对这块一目了然:

#include <iostream>


void Print(int& a) {
    std::cout << "int&: " << a << std::endl;
}


void Print(int&& a) {
    std::cout << "int&&: " << a << std::endl;
}

template <typename T>
void func1(T&& a) {
    Print(std::move(a));
}


template <typename T>
void func2(T&& a) {
    Print(std::forward<T>(a));
}

int main() {
    int arg = 10;

    std::cout << "Calling func1 with std::move()..." << std::endl;
    func1(arg); /* arg is an lvalue */
    func1(25);  /* 25 is an rvalue  */

    std::cout << "Calling func2 with std::forward()..." << std::endl;
    func2(arg); /* arg is an lvalue */
    func2(25);  /* 25 is an rvalue  */

    return 0;
}

上述代码输出如下:

Calling func1 with std::move()...
int&&: 10
int&&: 25
Calling func2 with std::forward()...
int&: 10
int&&: 25

在上述代码中:

创建了两个重载函数Print,其参数类型分别为**int &int &&**,函数的功能是输出其参数的类型模板函数func1(),函数参数a为转发引用(T&&,也有地方称之为万能引用),函数体内调用参数为std::move(a)的Print()函数,将a转换为右值引用,这意味着,如果a是左值,则传递给Print()函数的参数类型为右值引用模板函数func2(),与模板函数func1()一样,该函数也采用转发引用(T&&)。但是,它使用 std::forward来保留a的原始值类别。这意味着如果a是左值,它将作为左值传递给Print()函数,如果它是右值,它将作为右值传递在 main() 中,使用左值和右值调用函数func1和func2,以观察对应的行为

通过上面输出,基本可以区分这俩,在此,做下简单的总结:

目的std::forward:用于完全按照传递的参数转发,保留其值类别(左值或右值)std::move:用于将对象转换为右值引用,通常用于启用移动语义并转移所有权用法std::forward:通常用于转发引用(通用引用),以保留传递给另一个函数的参数的值类别std::move:用于将对象显式转换为右值引用影响std::forward:不更改参数的值类别。如果原始参数是右值引用,则它返回右值引用,否则返回左值引用std::move:将其参数转换为右值引用,将其值类别更改为右值安全std::forward:可以安全地与转发引用 (T&&) 一起使用,以确保正确转发参数,而不会产生不必要的副本。std::move:应谨慎使用,因为它可能会导致从其他地方仍需要的对象移动,从而导致未定义的行为场景std::forward:用于需要完美转发参数的场景,例如模板函数和类中。std::move:在显式转移所有权或调用移动语义时使用,例如从函数返回仅移动类型时返回类型std::forward:返回类型取决于传递给它的参数的值类别,它可以返回左值引用或右值引用。std::move:始终返回右值引用

以上~~


推荐阅读  点击标题可跳转

1、从示例入手了解惯用法之PIMPL

2、Mastering Placeholder Type Deduction

3、1000Mbps换算成MB/s是多少?除以8?想简单了

CPP开发者
我们在 Github 维护着 9000+ star 的C语言/C++开发资源。日常分享 C语言 和 C++ 开发相关技术文章,每篇文章都经过精心筛选,一篇文章讲透一个知识点,让读者读有所获~
 最新文章