一文看懂移动语义

一文看懂移动语义

1.前言

右值引用,我也不知道为啥突然多出来这么一个让人晦涩难懂的东西,而这个东西的诞生,伴随着C++移动语义的出现。没有右值,就没有移动语义。二者相辅相成,缺一不可。而移动语义又是现代C++大厦的基石,如果没有理解它,那就没法理解std::thread、std::unique_ptr等等具有独占所有权的类。那么话不多说,让我们来一窥,C++移动语义的神秘面纱。

1.1 为什么会出现移动语义

请思考这样一个问题:当我们需要传递一个“大对象”的时候,一般我们会这样子传递const BigObject&,这样子避免了一次拷贝,然而,如果外面传进来的“大对象”出了作用域之后就析构了,而这个“大对象”需要传递到一个类内进行保存起来呢,这个时候又应该怎么样传参才是效率最高的呢?我们来看一下,三种不同的传参有什么差异

1.1.1 默认值传递

#include <iostream>
// 大对象
class BigObject {
private:
    int *resource{nullptr};
public:
    // 构造函数
    BigObject(): resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) noexcept {
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
       std::cout << " ~BigObject() called." << std::endl;
    }
};
void processBigObject(BigObject obj) {
    // 处理大对象的函数
    std::cout << "pass by value \n";
    std::cout << "Processing big object..." << std::endl;
}
int main() {
    // 使用资源管理类来管理动态分配的内存
    {
        BigObject tmpBigObject;//
        processBigObject(tmpBigObject);//
    }
    std::cout<<"out of scope\n";
    return 0;
}

程序输出结果如下:

Default constructor called.
Copy constructor called.
pass by value
Processing big object...
 ~BigObject() called.
 ~BigObject() called.
out of scope

可以看到,当使用默认的值传递来进行参数传参的时候,会出现调用了一次默认构造函数、一次拷贝构造函数、两次析构函数。而最开始的默认构造函数构造出来的“大对象”并没有起到任何的作用,然后就析构了,就这样子白白浪费了

1.1.2 常引用传递

#include <iostream>
// 大对象
class BigObject {
private:
    int *resource{nullptr};
public:
    // 构造函数
    BigObject(): resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) noexcept {
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
       std::cout << " ~BigObject() called." << std::endl;
    }
};
//void processBigObject(BigObject const& obj) 这两个写法有区别嘛?
void processBigObject(const BigObject& obj) {
    // 处理大对象的函数
    std::cout << "pass by const&\n";
    std::cout << "Processing big object..." << std::endl;
}
int main() {
    // 使用资源管理类来管理动态分配的内存
    {
        BigObject tmpBigObject;//
        processBigObject(tmpBigObject);//
    }
    std::cout<<"out of scope\n";
    return 0;
}

程序输出如下:

Default constructor called.
pass by const&
Processing big object...
 ~BigObject() called.
out of scope

可以看到当使用常引用传递时候,只调用了一次默认构造函数,一次析构函数。效率大大的提高呀,可喜可贺。C++11 之后,将这种引用称为左值引用,为了与支持移动语义的右值引用区分。

1.1.3 右值引用传递

声明一个右值引用很简单,只需要在变量名的前面加上&&就可以了,现在我们只需要知道有这么一个东西就可以了。而右值引用的出现是为了实现构造函数的重载区分,使用&&右值引用的形参来匹配移动构造函数和移动赋值运算符。至于后面的更多细节,我会详细慢慢介绍。我们接着看一下上面例子的情况,当我们用右值引用传递的时候又会发生什么呢?

#include <iostream>
#include <iostream>
// 大对象
class BigObject {
private:
    int *resource{nullptr};
public:
    // 构造函数
    BigObject(): resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) noexcept {
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
       std::cout << " ~BigObject() called." << std::endl;
    }
};
//void processBigObject(BigObject const& obj) 这两个写法有区别嘛?
void processBigObject(const BigObject&& obj) {
    // 处理大对象的函数
    std::cout << "pass by const&&\n";
    std::cout << "Processing big object..." << std::endl;
}
int main() {
    // 使用资源管理类来管理动态分配的内存
    {
        BigObject tmpBigObject;//
        processBigObject(std::move(tmpBigObject));//亡值
    }
    std::cout<<"out of scope\n";
    return 0;
}

程序输出如下:

Default constructor called.
pass by const&&
Processing big object...
 ~BigObject() called.
out of scope

通过以上代码测试,可以知道,当以常右值引用传递的时候,调用了一次默认构造函数,一次移动构造函数,一次析构函数。哎呀,不得了了,开倒车呀,比常引用传递还多调用了一次移动构造函数,这是什么鬼呀,这移动语义也没啥用嘛。

2. 移动语义

稍安勿躁,真正的表演才刚刚开始,移动语义当然是有用武之地的,当然不是上面例子那样子来使用。接下来,我们来看一下移动语义的出现,到底解决了什么样的问题。

2.1 vector 扩容机制

大家可以看到移动构造函数形参的后面多了一个noexcept的关键字,这里我们暂时先不必理会它,后面我会专门讲这个关键字的作用的。
我们还是以上面的例子来进行说明,简单修改一下如下:

#include <iostream>
#include<vector>
// 大对象
class BigObject {
private:
    int* resource{ nullptr };
public:
    // 构造函数
    BigObject() : resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) noexcept {
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
        std::cout << " ~BigObject() called." << std::endl;
    }
};
int main() {
    std::vector<BigObject>objarr;
    objarr.reserve(1);
    std::cout << "reserve after: " << objarr.capacity() << '\n';
    // 使用资源管理类来管理动态分配的内存
    {
        BigObject tmpBigObject;//
        //扩容前
        for (int i = 0; i < 6; ++i) {
            objarr.push_back(tmpBigObject);
            std::cout << "capacity size: " << objarr.capacity() << '\n';
        }

    }
    std::cout << "out of scope\n";
    return 0;
}

程序输出如下:

reserve after: 1
Default constructor called.
Copy constructor called.
capacity size: 1
Copy constructor called.
Move constructor called.
 ~BigObject() called.
capacity size: 2
Copy constructor called.
Move constructor called.
Move constructor called.
 ~BigObject() called.
 ~BigObject() called.
capacity size: 3
Copy constructor called.
Move constructor called.
Move constructor called.
Move constructor called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
capacity size: 4
Copy constructor called.
Move constructor called.
Move constructor called.
Move constructor called.
Move constructor called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
capacity size: 6
Copy constructor called.
capacity size: 6
 ~BigObject() called.
out of scope
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.

通过上面的例子,我们可以知道两个事情,
1、当vector的空间不足的时候,在MSVC编译器的windows环境下,它是以1.5倍机制进行扩容的。(不要再和别人说,默认就是以2倍进行扩容的咯(gcc、clang可能就是这样的,但我没测试过,不好下定论),这个取决于各家编译器的实现)
2、当我们使用pushback的时候,它是调用拷贝构造函数拷贝进去vector容器里面的。
我们先把地址打印出来看看,到底是不是这样子的呢?

#include<vector>
// 大对象
class BigObject {
private:
    int* resource{ nullptr };
public:
    // 构造函数
    BigObject() : resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) noexcept {
        this->resource = other.resource;
        other.resource = nullptr;
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
        std::cout << " ~BigObject() called." << std::endl;
    }
    void ptintAddress()const {
        std::cout <<"addr: "<< std::hex << resource << '\n';
    }
};
int main() {
    std::vector<BigObject>objarr;
    objarr.reserve(5);
    std::cout << "capacity size: " << objarr.capacity() << '\n';
    // 使用资源管理类来管理动态分配的内存
    {
        BigObject tmpBigObject;//默认构造函数调用
        //扩容前
        for (int i = 0; i < 5; ++i) {
            objarr.push_back(tmpBigObject);//调用了拷贝构造函数
        }
    }
    for (const auto& obj : objarr)
    {
        obj.ptintAddress();
    }
    std::cout << "capacity size: " << objarr.capacity() << '\n';
    std::cout << "out of scope\n";
    return 0;
}

程序输出如下:

capacity size: 5
Default constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
 ~BigObject() called.
addr: 0000000000000000
addr: 0000000000000000
addr: 0000000000000000
addr: 0000000000000000
addr: 0000000000000000
capacity size: 5
out of scope
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.

哎呀,怎么回事,怎么拷贝进去的对象的指针全部变成 0 了。那肯定是我们的拷贝构造函数没写正确嘛,那么问题来了,上面那个类的拷贝构造函数应该怎么样写才正确呢?大家先好好思考一下,先别着急往下看,最好可以自己去实现一遍,这样子才能真正把它理解透。

#include <iostream>
#include<vector>
// 大对象
class BigObject {
private:
    int* resource{ nullptr };
public:
    // 构造函数
    BigObject() : resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        resource = new int[100000]();
        memcpy_s(resource, sizeof(resource), other.resource, sizeof(other.resource));
        std::cout << "sizeof(resource) " << sizeof(resource) << '\n';
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) noexcept {
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
        std::cout << " ~BigObject() called." << std::endl;
    }
    void ptintAddress()const {
        std::cout <<"addr: "<< std::hex << resource << '\n';
    }
};


int main() {
    std::vector<BigObject>objarr;
    objarr.reserve(5);
    std::cout << "capacity size: " << objarr.capacity() << '\n';
    // 使用资源管理类来管理动态分配的内存
    {
        BigObject tmpBigObject;//默认构造函数调用
        //扩容前
        for (int i = 0; i < 5; ++i) {
            objarr.push_back(tmpBigObject);//调用了拷贝构造函数
        }
    }
    for (const auto& obj : objarr)
    {
        obj.ptintAddress();
    }
    std::cout << "capacity size: " << objarr.capacity() << '\n';
    std::cout << "out of scope\n";
    return 0;
}

上面的程序输出如下:

capacity size: 5
Default constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
 ~BigObject() called.
addr: 00000169090F3B00
addr: 00000169092C0080
addr: 0000016909321B10
addr: 00000169093C0080
addr: 0000016909421B10
capacity size: 5
out of scope
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.

怎么回事呀,怎么这个拷贝构造出来的五个对象,地址还有重复的呀。

2.1.1 拷贝构造函数

上面的拷贝构造函数的实现是正确的嘛?大家发表一下自己的看法。
当然是不正确的,sizeof只能够计算出第一层内存的大小,所有sizeof(resource)打印出来的大小是8(64bit系统下)。那么怎么样写才是对的呢?

#include <iostream>
#include<vector>
// 大对象
class BigObject {
private:
    int* resource{ nullptr };
public:
    // 构造函数
    BigObject() : resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        resource = new int[100000]();
        memcpy_s(resource, 100000, other.resource, 100000);
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) noexcept {
        this->resource = other.resource;
        other.resource = nullptr;
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
        std::cout << " ~BigObject() called." << std::endl;
    }
    void ptintAddress()const {
        std::cout <<"addr: "<< std::hex << resource << '\n';
    }
};

int main() {
    std::vector<BigObject>objarr;
    objarr.reserve(5);
    // 使用资源管理类来管理动态分配的内存
    {
        BigObject tmpBigObject;//默认构造函数调用
        //扩容前
        for (int i = 0; i < 5; ++i) {
            objarr.push_back(tmpBigObject);//调用了拷贝构造函数
        }
    }
    for (const auto& obj : objarr)
    {
        obj.ptintAddress();
    }
    std::cout << "out of scope\n";
    return 0;
}

上面程序输出如下:

Default constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
Copy constructor called.
 ~BigObject() called.
addr: 000002428A153B00
addr: 000002428A1E0080
addr: 000002428A241B10
addr: 000002428A2E0080
addr: 000002428A341B10
out of scope
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.
 ~BigObject() called.

这里直接写死了100000,这样子写法,好与不好,大家见仁见智。但C++11之后,推荐使用带有长度信息的更安全的std::array数组。大家感兴趣的可以去修改一下这个类,测试一下,看一看array有没有什么优缺点。

另外一个就是当vector进行扩容的时候,如果没有实现移动构造函数,或者说实现的移动构造函数不对,那么默认就是调用的是拷贝构造函数,那么之前的“对象”就会被析构掉,释放内存,白白地浪费掉了。

2.1.2 noexcept关键字

还记得我们之前提到的noexcept关键字嘛,我们来测试一下,移动构造函数去掉这个关键字之后,vector扩容的时候,还会不会调用移动构造函数呢?
顺带一提,析构函数也是默认自带noexcept属性的。

#include <iostream>
#include<vector>
// 大对象
class BigObject {
private:
    int* resource{ nullptr };
public:
    // 构造函数
    BigObject() : resource(new int[100000]()) {
        std::cout << "Default constructor called." << std::endl;
    }

    BigObject(const BigObject& other) {
        resource = new int[100000]();
        memcpy_s(resource, 100000, other.resource, 100000);
        std::cout << "Copy constructor called." << std::endl;
    }

    BigObject(BigObject&& other) {
        this->resource = other.resource;
        other.resource = nullptr;
        std::cout << "Move constructor called." << std::endl;
    }
    // 析构函数,释放资源
    ~BigObject() {
        delete[] resource;
        resource = nullptr;
        std::cout << " ~BigObject() called." << std::endl;
    }
    void ptintAddress()const {
        std::cout << "addr: " << std::hex << resource << '\n';
    }
};

int main() {
    std::vector<BigObject>objarr;
    objarr.reserve(1);
    // 使用资源管理类来管理动态分配的内存
    {
        //扩容前
        for (int i = 0; i <1; ++i) {
            BigObject tmpBigObject;//默认构造函数调用
            objarr.push_back(std::move(tmpBigObject));//调用了拷贝构造函数
        }
    }
    std::cout << "--------------------扩容前-----------------\n";
    for (int i = 0; i < 1; ++i) {
        objarr.emplace_back(BigObject{});//调用了拷贝构造函数
    }
    std::cout << "--------------------扩容后-----------------\n";
    return 0;
}

程序输出结果如下:

Default constructor called.
Move constructor called.
 ~BigObject() called.
--------------------扩容前-----------------
Default constructor called.
Move constructor called.
Copy constructor called.
 ~BigObject() called.
 ~BigObject() called.
--------------------扩容后-----------------
 ~BigObject() called.
 ~BigObject() called.

通过上面测试,我们可以得出当vector扩容的时候,需要调用到移动构造函数的时候,如果我们写了移动构造函数,但又没有noexcept关键字修饰,那么原本vector扩容的时候应该调用移动构造的,变成了调用拷贝构造函数。所以,当移动构造没有noexcept关键字修饰时,移动的操作会退化成拷贝。
总结: 这里noexcept关键字的作用:标记为该函数体内不会抛出异常,一旦抛出异常,那么将会直接调用std::terminate来结束程序的运行。

2.1.3 移动语义使用场景1

我们花了这么大的力气,一个小小的程序,翻来覆去,是为了说明什么问题呢?为了说明,这个是移动语义运用的第一个场景。
当你的移动语义实现正确,那么当容器扩容的时候,就会调用廉价的移动语义,而不是昂贵的拷贝。

2.2 返回值优化

2.2.1 无名返回值优化(UROV)

我们先来看一个例子,后面再来看看这个是什么意思。

#include <iostream>
#include <string>

// 定义一个简单的结构体
struct MyStruct {
    int number;
    std::string message;

    MyStruct(int num, const std::string& msg) : number(num), message(msg) {
        std::cout << "Regular constructor called" << std::endl;
    }

    // 拷贝构造函数
    MyStruct(const MyStruct& other) : number(other.number), message(other.message) {
        std::cout << "Copy constructor called" << std::endl;
    }

    // 移动构造函数
    MyStruct(MyStruct&& other) noexcept : number(std::move(other.number)), message(std::move(other.message)) {
        std::cout << "Move constructor called" << std::endl;
    }
};

// 返回一个MyStruct对象的函数
MyStruct createStruct() {
    return MyStruct(42, "Hello, world!"); //这里呢?调用的是什么构造函数?
}
int main() {
    // 调用函数并将返回值赋给变量
    MyStruct s = createStruct(); //大家觉得这里调用的是什么构造函数?拷贝构造函数还是拷贝赋值运算符呢?

    // 打印结构体成员
    std::cout << "Number: " << s.number << std::endl;
    std::cout << "Message: " << s.message << std::endl;

    return 0;
}

程序输出结果如下:

Regular constructor called
Number: 42
Message: Hello, world!

通过测试我们发现,只调用了一次构造函数,就地构造。天呀,怎么回事呀,为啥不是在函数体内部调用构造函数,返回的时候再调用拷贝构造函数呢?这是返回值优化的其中一个场景。这种情况称为UROV(无名返回值优化)。注:从c++17开始,copy elision得到保证,强制使用URVO,但并不强制使用NRVO。(也就是说如果是C++17以前,测试的结果和现象可能不一致,或者也有可能直接编译报错,因为在C++11的时候,标准是这样说的 即使复制/移动 (C++11 起)构造函数和析构函数拥有可观察的副作用。这些对象将直接构造到它们本来要复制/移动到的存储中。这是一项优化:即使进行了优化而不调用复制/移动 (C++11 起)构造函数,它仍然必须存在且可访问(如同完全未发生优化),否则程序非良构)
返回值优化的另外一种说法是复制消除(copy_elision),从 C++17 起,非必须不会将纯右值实质化,并且它会被直接构造到其最终目标的存储中。这有时候意味着,即便语言的语法看起来进行了复制/移动(例如复制初始化),也并不进行复制/移动——这表示该类型完全不需要具有可访问的复制/移动构造函数。

这里提到了纯右值,大家不要着急,我们先来看现象与运用,至于什么是纯右值,后面我们再来一一探讨。
2.2.2 具名返回值优化(NRVO)

#include <iostream>
#include <string>

// 定义一个简单的结构体
struct MyStruct {
    int number;
    std::string message;

    MyStruct(int num, const std::string& msg) : number(num), message(msg) {
        std::cout << "Regular constructor called" << std::endl;
    }

    // 拷贝构造函数
    MyStruct(const MyStruct& other) : number(other.number), message(other.message) {
        std::cout << "Copy constructor called" << std::endl;
    }

    // 移动构造函数
    MyStruct(MyStruct&& other) noexcept : number(std::move(other.number)), message(std::move(other.message)) {
        std::cout << "Move constructor called" << std::endl;
    }
};

// 返回一个MyStruct对象的函数
MyStruct createStruct() {
    MyStruct ms(42, "Hello, world!");
    return ms;//这里呢?调用的是什么构造函数?
}
int main() {
    // 调用函数并将返回值赋给变量
    MyStruct s = createStruct(); //大家觉得这里调用的是什么构造函数?拷贝构造函数还是拷贝赋值运算符呢?

    // 打印结构体成员
    std::cout << "Number: " << s.number << std::endl;
    std::cout << "Message: " << s.message << std::endl;

    return 0;
}

程序输出结果如下:

Regular constructor called
Number: 42
Message: Hello, world!

具名返回值优化就是说,在局部构建了一个有名字的临时变量,函数返回的时候,在某些情况下,不会调用拷贝构造函数,而是直接调用构造函数,在调用处直接构造对象。

#include <iostream>
 
struct Noisy
{
    Noisy() { std::cout << "在 " << this << " 构造" << '\n'; }
    Noisy(const Noisy&) { std::cout << "复制构造\n"; }
    Noisy(Noisy&&)noexcept { std::cout << "移动构造\n"; }
    ~Noisy() { std::cout << "在 " << this << " 析构" << '\n'; }
};
 
Noisy f()
{
    Noisy v = Noisy(); // (C++17 前) 从临时量初始化 v 时发生复制消除,可能调用移动构造函数
                       // (C++17 起) "有保证的复制消除"
    return v; // 从 v 到结果对象的复制消除,可能调用移动构造函数
}
 
void g(Noisy arg)
{
    std::cout << "&arg = " << &arg << '\n';
}
 
int main()
{
    Noisy v = f(); // (C++17 前) 从 f() 的结果初始化 v 时发生复制消除
                   // (C++17 起) "有保证的复制消除"
 
    std::cout << "&v = " << &v << '\n';
 
    g(f()); // (C++17 前) 从 f() 的结果初始化实参时发生复制消除
            // (C++17 起) "有保证的复制消除"
}

当我们知道了返回值优化的事情,这样子有什么用嘛?对于在日常开发过程中,又有什么意义呢?让我们来看看下面的代码例子

#include <iostream>
#include <string>

struct ConfigInfo {
	std::string name{""};
	std::string brand{""};
	std::string pwd{""};

	// 默认构造函数
	ConfigInfo() = default;
	// 构造函数
	ConfigInfo(const std::string& n, const std::string& b, const std::string& p)
		: name(n), brand(b), pwd(p) {}

	// 拷贝构造函数
	ConfigInfo(const ConfigInfo& other)
		: name(other.name), brand(other.brand), pwd(other.pwd) {
		std::cout << "Copy constructor called." << std::endl;
	}

	// 移动构造函数
	ConfigInfo(ConfigInfo&& other) noexcept
		: name(std::move(other.name)), brand(std::move(other.brand)), pwd(std::move(other.pwd)) {
		std::cout << "Move constructor called." << std::endl;
	}

	// 析构函数(可选)
	~ConfigInfo() {
		std::cout << "Destructor called for " << name << std::endl;
	}
};

using ConfigInfo = struct ConfigInfo;
ConfigInfo&& getInfoFromFile() {
	//假设打开配置文件读取配置信息,然后构建临时变量 对配置信息进行赋值
	ConfigInfo configinfo;
	configinfo.brand = "Nova";
	configinfo.name = "Nova1";
	configinfo.pwd = "Nova123";
	return std::move(configinfo);
}
int main()
{
	auto configInfo = getInfoFromFile();
	return 0;
}

程序输出结果如下:

Destructor called for Nova1
Move constructor called.
Destructor called for

可以看到上面的代码例子,函数体内的临时变量已经出了作用域了,调用析构函数了,外面还引用着这个临时变量,这是未定义行为来的。这个例子是在函数返回的时候,调用了std::move,尝试让编译器调用廉价的移动语义。如果我们不知道返回值优化相关的内容,就会写出这样的代码,出现负优化的情况,还有潜在的隐患,会出现悬垂引用,导致程序崩溃的问题。

注:别把右值引用不当引用,右值引用也是引用!!!

2.3 左值、纯右值与亡值

C++11标准之后,将值的类型分为三种,分别是左值、纯右值和亡值。其中,当我们调用std::move将一个变量强转成右值,这个情况就称为亡值。

2.3.1 左值

左值,简单来说就是可以放到等号左边,可以被取地址的值。(例外情况遇到之后再单独记住就好了)例如:字符串字面量就是左值,可以取地址,++i也是左值,可以取地址。函数名也是左值,可以取地址。其他更多的情况,请查阅参考资料中的cppreference关于值类别的说明。
左值,我们理解到这里,其实在日常开发中就足够了,遇到没有办法确定的时候,也可以用一些代码的手段去让编译器帮我们去甄别。

2.3.2 纯右值

纯右值,字面量,例如 42、true 或 nullptr、i++等等,简单地了解就可以了,我们不是语言律师,不需要去清楚每一个标准的细枝末节。

2.3.3 亡值

亡值,std::move将一个变量强转成右值,这个情况就称为亡值。函数体内的临时变量也称为亡值。

2.3.4 右值引用绑定规则

至于这个,在这里暂时不展开。

移动语义的使用场景

下面我们来看看,在日常开发中,移动语义的使用场景,看看同学们能不能从中获得启发。

在这里插入图片描述

上面的代码,是一个通过http协议上传json信息到工厂的mes系统的接口,可以看到上传的json内容使用了右值引用&&,这里我们明确地知道,json内容是上传给mes的,外面调用的时候,不会再需要用到这个json,所以json内容的形参可以以右值引用的方式传递。
除此之外,这个代码的例子,这样子写有没有什么问题呢?为什么会有一个波浪线的警告提示呢?这是静态代码检查工具给出的警告。
然后我们可以看一下静态检查工具给出的建议提示
在这里插入图片描述

可以看到这里说,使用了&&右值引用传参,但是在函数体内却又没有看到调用了std::move,这是因为当右值引用传递给函数形参的时候,它就变成了一个左值,因为函数形参是可以取地址的。
在这里插入图片描述

1.6 参考资料

参考资料如下
[1]: https://zhuanlan.zhihu.com/p/455848360
[2]: https://zh.cppreference.com/w/cpp/language/copy_elision
[3]: https://zh.cppreference.com/w/cpp/language/value_category
[4]: <<Effective Modern C++>>

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值