文章

详解C++移动语义相关基础

C++11引入了移动语义,本文将详细介绍与其相关的左右值引用、std::move、移动构造函数与移动赋值运算符、类的六大特殊成员函数、RVO与NRVO、完美转发等概念

详解C++移动语义相关基础

本博客以更精炼的语言由一文入魂:妈妈再也不担心我不懂C++移动语义了等文章总结而成

一、关于移动语义

  • 移动语义是C++11引入的特性,其目的为在传值时不期望发生拷贝时,通过转移资源所有权而非拷贝资源来提高性能,以避免不必要的深拷贝、并减少对象资源分配与销毁的开销
    • 右值引用:用&&表示,用于绑定临时对象或即将销毁的对象
    • 移动构造函数:接受右值引用参数的类对象构造函数,将资源从源对象移动到新对象,通常将源对象的指针置为nullptr
    • 移动赋值运算符:定义不发生拷贝的赋值操作,功能同移动构造函数
  • 在如下例程中,我们创建了一个容器以及一个MyClass对象tmp,并将其添加到容器中两次,在每次添加时都会发生一次拷贝操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <string>
#include <vector>

class MyClass
{
private:
    std::string str;

public:
    MyClass(const std::string& _s) : str(_s) {}
};

int main()
{
    std::vector<MyClass> myClasses;
    MyClass tmp("hello");
    
    myClasses.push_back(tmp);
    myClasses.push_back(tmp);
}
  • 添加完成后,对象tmp已不再被需要
    • 此时若放任不管则存在空间损耗,若将其销毁则存在时间损耗,若在第二次添加时直接将tmp对象移动到列表中而非拷贝,则不存在上述损耗
    • 这可通过在MyClass类内实现移动语义、在类外使用std::move()函数来实现,具体代码实现方式详见后文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <string>
#include <vector>

class MyClass
{
private:
    std::string str;

public:
    MyClass(const std::string& _s) : str(_s) {}
	
	//假设此处实现了移动语义所需的东西
};

int main()
{
    std::vector<MyClass> myClasses;
    MyClass tmp("hello");
    
    myClasses.push_back(tmp);
    //最后一次添加使用移动语义而非拷贝
    myClasses.push_back(std::move(tmp));
}
  • 两种方法的内存模型对比如下,移动语义在更复杂的场景中能极大地提升程序运行性能

对比拷贝与移动.png

二、移动语义的实现

2.1 左右值引用

2.1.1 左值与右值

  • 回顾一下关于左右值的概念
    • 左值
      • 表达式结束后依然存在的持久化对象,其可被&取址,有名字,且能被赋值
      • 例如类成员变量、作用域内的变量等,其中变量在局部作用域内使用时是左值,但若将其返回到作用域外,则在作用域外就是右值
    • 右值
      • 表达式结束后就不再存在的临时对象,其不可被&取址,没有名字
      • C++11对右值概念进行了扩展,将其分为了纯右值和将亡值
        • 纯右值:非引用返回的临时变量、运算表达式产生的结果、字面常量等
        • 将亡值:与右值引用相关的表达式,例如将要被移动的对象、T&&函数返回的值、函数std::move()的返回值、转换成T&&的类型的转换函数的返回值等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cls
{
	int data;
};
 
Cls GetTemp()
{
	return Cls();
}

//x是左值,6是右值
int x = 6;
//y是左值,(x + 8)是右值
int y = x + 8;
//k是左值 ,GetTemp()的返回值是右值(临时变量)
Cls k = GetTemp();

2.1.2 左值引用与右值引用

  • 为支持移动语义,C++11引入了右值引用,而此前使用的普通声明则改称左值引用
    • 左值引用(已命名的左值引用T& a中的a仍是左值)
      • T&类型声明,只能绑定左值
      • const T&类型声明的是常量左值,既可绑定左值又可绑定右值
    • 右值引用(已命名的右值引用T&& a中的a是左值)
      • T&&类型声明,只能绑定右值或std::move标记的非常量左值(因为移动操作需要修改被移动的原变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int GetTempValue()
{
	//...return...
}

//某作用域内
{
	//非常量的局部临时变量,属于左值
	int x = 0;
	
	//正确,左值可以引用左值x
	int& lRef0 = x;
	//错误,左值不能引用右值(z * 6)
	int& lRef1 = z * 6;
	//正确,const左值可以引用右值(z * 6)
	const int& lRef2 =  z * 6;
	
	//正确,右值引用可以接收右值(x * 6)
	int&& rRef0 = x * 6;
	//正确,右值引用可以接收函数返回的临时对象右值
	int&& rRef1 = GetTempValue(); 
	//错误,右值引用不能直接接收左值x
	int&& rRef2 = x;
	//正确,可以右值引用被std::move标记的(非const)左值
	int&& rRef3 = std::move(x);
}
  • 其中函数std::move本质上是一个类型转换,作用是将一个左值强制转换为右值引用,从而可以利用右值引用进行移动语义操作,参考一文详解C++中的std::move函数

2.2 移动构造函数

2.2.1 类成员变量有移动构造

  • 在示例例程中,我们使用如下代码实现了移动语义,这意味着std::vector::push_back方法提供了接收右值引用的模板函数重载,该重载执行时会调用传入模板类(此处为MyClass)的移动构造函数(而非拷贝构造函数)以增加元素
1
myClasses.push_back(std::move(tmp));
  • 为了使std::vector::push_back能够找到移动构造函数,我们需要在类内声明并定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <string>
#include <vector>

class MyClass
{
private:
    std::string str;

public:
    MyClass(const std::string& _s) : str(_s) {}

    //移动构造函数,调用了std::string类型的移动构造函数(关于nonexcept详见后文)
    MyClass(MyClass&& _rValue) noexcept : str(std::move(_rValue.str)) {}
};

int main()
{
    std::vector<MyClass> myClasses;
    MyClass tmp("hello");
    
    //此行欲调用MyClass类的拷贝构造函数,失败报错
    myClasses.push_back(tmp);
    myClasses.push_back(std::move(tmp));
}
  • 此时我们运行程序会发现报错,因为我们定义了移动构造函数(或移动赋值运算符)后,编译器会自动删除默认的拷贝构造函数(与拷贝赋值运算符),而在上述例程中第一次将tmp添加到容器时使用的是拷贝而非移动的方式,故需显式提供拷贝构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <string>
#include <vector>

class MyClass
{
private:
    std::string str;

public:
    MyClass(const std::string& _s) : str(_s) {}

    //显式定义拷贝构造函数(若有堆成员则需是深拷贝)
    MyClass(const MyClass& _other) : str(_other.str) {}

    //移动构造函数,调用了std::string类型的移动构造函数(关于nonexcept详见后文)
    MyClass(MyClass&& _rValue) noexcept : str(std::move(_rValue.str)) {}
};

int main()
{
    std::vector<MyClass> myClasses;
    MyClass tmp("hello");
    
    //或使用myClasses.emplace_back(tmp);在此处等效
    myClasses.push_back(tmp);
    myClasses.push_back(std::move(tmp));
}
  • 值得一提的是std::vector::emplace_back可以直接在容器中构造对象以避免临时对象的创建和拷贝/移动,前提是必须传递构造参数,即emplace_back("hello"),而非传递一个已构造好的对象tmp,所以此处即便使用emplace_back(tmp)也会调用拷贝构造

2.2.2 类成员变量无移动构造

  • 在前文的MyClass例子中我们将移动操作交由std::string类型完成,但对于其他需手动实现移动的成员变量类型,我们需要详细自定义移动的行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <string>
#include <vector>

//用于std::strcpy()
#include <cstring>

class MyClass
{
private:
    //更改std::string为char*
    char* str;
    //新增一个其他类型成员变量
    int val;

public:
    //构造函数
    MyClass(const std::string& _s, int _v)
    {
        //初始化val
        val = _v;

        //分配字符串内存
        str = new char[_s.length() + 1];
        //复制字符串内容
        std::strcpy(str, _s.c_str());
    }

    //拷贝构造函数,实现深拷贝
    MyClass(const MyClass& _other)
    {
        val = _other.val;

        //判断nullptr
        if (_other.str)
        {
            //深拷贝字符串char指针
            str = new char[std::strlen(_other.str) + 1];
            std::strcpy(str, _other.str);
        }
        else
            str = nullptr;
    }

    //移动构造函数
    MyClass(MyClass&& _rValue) noexcept
    {
        //对于int类型,直接拷贝即可
        val = _rValue.val;
        //将原对象的值置为默认值
        _rValue.val = 0;
        
        //直接拷贝指针
        str = _rValue.str;
        //将原对象的指针置空,这很关键,防止重复析构函数delete同一块内存空间
        _rValue.str = nullptr;
    }

    //析构函数释放内存
    ~MyClass()
    {
	    //语句delete nullptr是安全的
		delete[] str;
		//这句并非必须
		str = nullptr;
    }
};

int main()
{
    std::vector<MyClass> myClasses;
    MyClass tmp("hello", 99);
    
    myClasses.push_back(tmp);
    myClasses.push_back(std::move(tmp));
}

2.3 移动赋值运算符

  • 重载移动赋值运算符即可,需注意的是其接收参数需要是类的非常量右值引用T&&,而非拷贝赋值运算符接收的常量左值引用const T&,且返回值是类的左值引用(与内置类型的=行为保持一致,以支持链式复制)
  • 移动赋值的发生存在于已经存在的两个类对象之间,例如a = std::move(b);,而非在类对象初始化时用=,例如MyClass a = std::move(b);调用的就是移动构造函数而不是移动=运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <string>
#include <iostream>

//用于std::strcpy()
#include <cstring>

class MyClass
{
private:
    char* str;
    int val;

public:
    //构造函数
    MyClass(const std::string& _s, int _v)
    {
        //初始化val
        val = _v;

        //分配字符串内存
        str = new char[_s.length() + 1];
        //复制字符串内容
        std::strcpy(str, _s.c_str());
    }

    //移动赋值运算符
    MyClass& operator=(MyClass&& _other) noexcept
    {
        //拷贝val并置空
        val = _other.val;
        _other.val = 0;

        //转移str并置空
        str = _other.str;
        _other.str = nullptr;

        //返回自身引用
        return *this;
    }

    //析构函数释放内存
    ~MyClass()
    {
		delete[] str;
		str = nullptr;
    }

    void Print() const
    {
        if (str)
            std::cout << str << " " << val << "\n";
        else
            std::cout << "Moved\n";
    }
};

int main()
{
    MyClass tmp("hello", 99);
    MyClass obj("", 0);
    obj.Print();
    tmp.Print();

    //测试移动赋值运算符
    obj = std::move(tmp);
    obj.Print();
    tmp.Print();

    // 0
    //hello 99
    //hello 99
    //Moved
}

2.4 被移动对象的状态

  • 当一个对象被执行了移动操作而将数据转移给其他对象后,原对象仍然有效并可以被使用而不会报错,直到最终被执行析构函数,所以一定要确保移动操作实现将原对象开辟在堆区的成员指针置空为nullptr,防止原对象析构时delete同一块内存空间导致错误

三、类特殊成员函数生成规则

3.1 函数的已删除状态

  • 在C++11中使用语法= delete;可以将函数(包括类的六个特殊成员函数)定义为已删除,任何使用已删除函数的代码都会产生编译错误
1
2
3
4
5
6
7
8
9
class MyClass
{
public:
    void Test() = delete;
};

MyClass value;
//编译错误:attempting to reference a deleted function
value.Test();
  • 我们需要注意在特定情况下,编译器会将类的特定特殊成员函数定义为已删除,详见后文

3.2 六大特殊成员函数

  • 在C++11前存在四个特殊的类成员函数,若定义一个空类,编译器会自动生成这四个函数
    • 构造函数
    • 析构函数
    • 拷贝构造函数
    • 拷贝赋值运算符
  • 在C++11后新增两个特殊的类成员函数,若定义一个空类,编译器除了生成上述四个特殊成员函数外,还会生成这两个新增的特殊类成员函数
    • 移动构造函数
    • 移动赋值运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//支持std::move
#include <utility>

class MyClass {};

int main()
{
    //执行编译器默认生成的:构造函数
    //此处若使用MyClass A();会被当作函数声明,所以只能使用MyClass A{};或MyClass A;构造
    MyClass A;
    //执行编译器默认生成的:拷贝构造函数
    MyClass B(A);
    //执行编译器默认生成的:移动构造函数
    MyClass C(std::move(A));
}

3.3 默认构造函数与析构函数

  • 若没有显式定义任何构造函数(接收了其他参数的非默认构造、拷贝构造、移动构造),则编译器会自动生成默认(即不接收参数)构造函数,否则不会生成,除非使用= default;显式请求
  • 默认析构函数总是自动生成,除非显式使用= delete;进行删除

3.4 拷贝构造与拷贝赋值运算符

  • 若在类中未显式定义:移动构造,或移动赋值运算符
    • 则编译器会自动生成:拷贝构造,与拷贝赋值运算符
  • 否则,若在类中显式定义了移动操作
    • 则拷贝操作会被禁用,除非使用= default;显式请求

3.5 移动构造与移动赋值运算符

3.5.1 一般情况

  • 若在类中未显式定义:拷贝构造,或拷贝赋值运算符,或析构函数
    • 则编译器会自动生成:移动构造,与移动赋值运算符

3.5.2 定义了拷贝操作

  • 若在类中显式定义了拷贝操作或析构函数
    • 则移动操作会被禁用(除非使用= default;显式请求),若在此时调用移动操作,则编译器会转而执行对应的拷贝构造函数,或拷贝赋值运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>

class MyClass
{
public:
	//由于显式定义了拷贝构造,默认构造不会生成,所以需显式声明进行生成(或也可自定义)
	MyClass() = default;
	
	//定义拷贝构造函数,这会禁止自动生成移动构造函数、移动赋值运算符
    MyClass(const MyClass& _value)
	{
		std::cout << "CopyConstructor\n";
	}

	//拷贝赋值运算符
	MyClass& operator=(const MyClass& _other)
	{
		std::cout << "CopyOperator=\n";
		return *this;
	}
};

int main()
{
	//不能使用MyClass X();
	MyClass A1;
	MyClass A2;
	
	//试图调用移动构造函数,由于移动已被删除,此处调用拷贝构造函数
	MyClass B(std::move(A1));
	//CopyConstructor

	//若在初始化时就使用等号,则实际上是试图调用移动构造函数而非移动赋值运算符,结果同上
	MyClass C = std::move(A2);
	//CopyConstructor

	//试图调用移动赋值运算符,由于移动已被删除,此处调用拷贝赋值运算符
	MyClass D;
	D = std::move(A2);
	//CopyOperator=
}

3.5.3 定义了析构函数

  • 以下是定义了析构函数的情况(注意当一个类需要作为基类时,一般需要声明虚析构函数,此时需特别留意是否应该手动为该基类定义移动构造函数以及移动赋值运算符)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

class MyClass
{
public:
	//若定义析构函数也会禁止自动生成移动构造函数与移动赋值运算符
	~MyClass() {}
};

int main()
{
	//不能使用MyClass A();
	MyClass A;
	//由于移动已被删除,此处调用(未被删除、自动生成的)默认拷贝构造函数
	MyClass B(std::move(A));
}
  • 若子类继承自提供虚析构函数的基类,并且并未重写析构函数,则该子类的移动构造函数以及移动赋值运算符仍会被编译器自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//支持std::move
#include <utility>

//基类,提供虚析构
class MyBaseClass
{
public:
    virtual ~MyBaseClass() {}
};

//子类,未重写析构
class MyClass : MyBaseClass {};

int main()
{
	MyClass A;
	//此处执行编译器自动生成的移动构造函数
	MyClass B(std::move(A));
}

3.5.4 移动操作相互影响

  • 若单独定义了移动构造函数,则移动赋值运算符会被定义为已删除
  • 若单独定义了移动赋值运算符,则移动构造函数会被定义为已删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//支持std::move
#include <utility>

class MyClass
{
public:
	//由于定义了移动构造,默认构造需显式指定生成
	MyClass() = default;

    //定义移动构造函数,这会禁止编译器自动生成移动赋值运算符,并对移动赋值运算符的调用产生编译错误
    MyClass(MyClass&& rValue) noexcept {}
};

int main()
{
	MyClass A;
	MyClass B;
	//试图调用移动赋值运算符,编译错误
	//use of deleted function 'constexpr MyClass& MyClass::operator=(const MyClass&)'gcc
	B = std::move(A);
}

3.5.5 情况总结

移动操作的生成规则.png

3.5.6 生成的形式

  • 编译器自动生成的移动构造函数或移动赋值运算符的实现中,执行的是逐成员的移动语义,以如如下这个类为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass
{
private:
    int val;
    std::string str;
    
public:
    //编译器自动生成的移动构造函数类似这样,执行逐成员的移动语义
    MyClass(MyClass&& rValue) noexcept
    {
	    val = std::move(rValue.val);
	    str = std::move(rValue.str);
    }

    //编译器自动生成的移动赋值运算符类似这样,执行逐成员的移动语义
    MyClass& operator=(MyClass&& rValue) noexcept
    {
        val = std::move(rValue.val);
        str = std::move(rValue.str);
        return *this;
    }
};

四、noexcept关键字

4.1 引入背景

  • 前文实现移动构造函数与移动赋值运算符时,我们使用了noexcept关键字进行修饰,现在仅将代码中的noexcept关键字去除,分析这段代码的执行过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <string>
#include <vector>

class MyClass
{
private:
    std::string str;

public:
    MyClass(const std::string& _s) : str(_s) {}

    //显式定义拷贝构造函数(若有堆成员则需是深拷贝)
    MyClass(const MyClass& _other) : str(_other.str) {}

    //移动构造函数,调用了std::string类型的移动构造函数
    MyClass(MyClass&& _rValue) noexcept : str(std::move(_rValue.str)) {}
};

int main()
{
    std::vector<MyClass> myClasses;
    MyClass tmp("hello");
    
    myClasses.push_back(tmp);
    myClasses.push_back(std::move(tmp));
}
  • 当执行第一个push_back时,正常执行拷贝构造,向列表中存入第一个元素

noexcept关键字的意义P1.png

  • 当执行第二个push_back时,旧的容器扩容后(参考std::vector链表的扩容机制,每当容器容量满了后扩容$2$倍然后转移旧元素到新列表中,转移的过程默认使用拷贝构造)将第二个元素以移动构造函数添加到扩容后的容器中
  • 我们会发现在每次扩容的过程中,旧列表的元素若能通过移动操作转移到扩容后的列表中,就能节省旧元素销毁的资源浪费

noexcept关键字的意义P2.png

4.2 强异常保证

那么为什么std::vector扩容的过程中不默认使用移动语义呢?

  • 强异常保证(Strong Exception Guarantee)指的是当调用一个函数时,若发生了异常,那么应用程序的状态能够回滚到函数调用之前
    • 而函数std::vector::push_back就是强异常保证的,在该函数执行过程中若(由于内存不足需要申请新的内存、将老的元素放到新内存中等诱因)发生了异常,其需确保应用程序状态能回滚到调用它之前
    • 以前文的例子来说,当第二次执行push_back操作时,若发生异常,则应用程序的状态会回滚到第一次执行push_back语句之后,即此时该上下文的容器中只有一个元素
  • 我们在前文的代码中使用了noexcept修饰移动构造函数,这相当于告诉编译器我们能保证移动构造函数不会抛出异常,继而使得容器扩容时能够使用移动语义,否则push_back就会为了确保强异常保证,转而使用拷贝构造函数
    • 但是代码中的拷贝构造函数并未保证不会抛出异常,为何就能用呢?这是由于拷贝构造函数执行之后,原始数据仍会保留,即便发生异常需要回滚,仍能回滚完整有效的原始数据
    • 而移动语义就不同了,执行移动操作之后,原始数据会被清除,一旦发生异常,原始数据已然丢失,也就无法完成状态回滚了
  • 若以noexcept修饰的函数在执行过程中抛出了异常,那么应用程序会直接终止执行

4.3 使用noexcept

  • 我们使用noexcept关键字修饰移动构造函数,就能使得容器扩容转移原有元素时使用移动语义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <string>
#include <vector>

class MyClass
{
private:
    std::string str;

public:
    MyClass(const std::string& _s) : str(_s) {}

    //显式定义拷贝构造函数(若有堆成员则需是深拷贝)
    MyClass(const MyClass& _other) : str(_other.str) {}

    //移动构造函数,调用了std::string类型的移动构造函数
    //使用noexcept关键字修饰,承诺不会出现异常
    MyClass(MyClass&& _rValue) noexcept : str(std::move(_rValue.str)) {}
};

int main()
{
    std::vector<MyClass> myClasses;
    MyClass tmp("hello");
    
    myClasses.push_back(tmp);
    myClasses.push_back(std::move(tmp));
}

noexcept关键字的意义P3.png

五、返回值优化

RVO和NRVO是函数返回值的两种优化技术,能避免不必要的拷贝构造函数调用,二者的使用取决于编译器和优化级别等因素,不同的编译器可能有不同的策略

5.1 返回值优化RVO

  • 返回值优化(RVO, Return Value Optimization)的主要思想是将函数内部创建的局部对象直接构造到调用函数的目标对象中,而非先在函数内部构造临时对象然后再拷贝到目标对象,以避免额外的构造和拷贝开销
  • GCC中的RVO实现大致为直接将返回的对象构造在函数调用者的栈帧上,这样调用者就可以直接访问这个对象而不必拷贝
  • 在下面的代码中,若启用了RVO,则CreateObject函数返回的临时对象会被直接构造在调用该函数的obj2上,参考输出结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
 
class MyClass
{
public:
	MyClass()
	{
	    std::cout << "Constructor Called\n";
	}
	MyClass(const MyClass& other)
	{
	    std::cout << "Copy Constructor Called\n";
	}
};

MyClass CreateObject()
{
	//若触发RVO,则会将临时对象直接构造到目标对象,而不进行拷贝
    return MyClass();
}
 
int main()
{
    MyClass obj1 = MyClass();
	MyClass obj2 = CreateObject();
	//Constructor Called
	//Constructor Called
}

5.2 this指针不被优化

  • 但并不是所有的情况都会优化,例如在成员函数里面返回this指针的解引用时不被优化
  • 对于下面的例程,主函数内obj.Func().Func().Func();试图将obj从左到右传递,企图在传递过程中传递obj对象本身而最终得到data==13的结果,但事实上每次Func函数内将*this返回的时候,发生的是拷贝而非传递自身,所以最终得到的只能是data==11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
 
class MyClass
{
public:
    int data;

public:
	MyClass(int _val)
	{
		data = _val;
		std::cout << "Constructor\n";
	}
 
	MyClass(const MyClass& _other)
	{
		this->data = _other.data;
		std::cout << "Copy Constructor\n";
	}
 
	~MyClass()
	{
		std::cout << "Destructor\n";
	}
 
    MyClass Func()
	{
		//将数据递增
		(this->data)++;
		//返回自身
		return *this;
    }
};
 
int main()
{
	//创建一个data等于10的对象
	MyClass obj(10);
	//试图链式调用累积递增
	obj.Func().Func().Func();
	//检测是否累积成功
	std::cout << obj.data << "\n";

	//Constructor
	//Copy Constructor
	//Copy Constructor
	//Copy Constructor
	//Destructor
	//Destructor
	//Destructor
	//11
	//Destructor
}

5.3 命名返回值优化NRVO

  • 命名返回值优化(NRVO, Named Return Value Optimization)是RVO的一种特殊情况,其在被触发时会将被返回的局部对象直接构造到调用该函数的目标对象中,而非销毁原对象并在调用方新建对象,NRVO发生的条件是
    • 直接返回一个在函数内部有明确名称的(不被std::move修饰的)局部对象
    • 返回类型与局部变量类型完全匹配
  • 例如在下面的代码中
    • 若无NRVO,则B对象在获取了函数的返回值后会由MyClass类的拷贝构造函数创建,然后函数GetTemp内创建的临时对象_A会直接被销毁
    • 若有NRVO,则函数GetTemp内的临时对象_A在函数调用结束后并不会被销毁,而是转移给了外部调用该函数的对象B
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass {};

MyClass GetTemp()
{
	//创建了一个_A对象
	MyClass _A;
	//将其返回
	return _A;
}

int main()
{
	//此处B对象接收了函数GetTemp内创建的临时对象_A
	MyClass B = GetTemp();
}

5.4 避免std::move返回值

  • 通过如下的方式使用移动语义,能否使得即便没有NRVO也能避免拷贝操作呢?
  • 下面的代码中,由于std::move(_A)返回的是MyClass&&右值引用,与函数返回的MyClass类型不一致,因此阻止了编译器使用NRVO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <utility>

class MyClass {};

MyClass GetTemp()
{
    MyClass _A;
    //警告信息:return了std::move可能会阻止编译器NRVO等优化,导致性能下降
    //moving a local object in a return statement prevents copy elision [-Wpessimizing-move]gcc
    return std::move(_A);
}

int main()
{
	MyClass B = GetTemp();
}
  • 返回std::move的行为容易忽略返回的对象类型未实现移动语义的情况,容易产生问题
  • 我们如下改写MyClass类,由于移动构造函数已被取消,此时执行MyClass B = GetTemp();会导致编译器调用拷贝构造函数来创建B对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <utility>

class MyClass
{
public:
	//默认构造函数
	MyClass() = default;

	//拷贝构造函数
	MyClass(const MyClass& _other)
	{
		std::cout << "CopyConstructor\n";
	}

	//声明析构函数,导致禁止编译器实现默认移动构造函数
	~MyClass() {}
};

MyClass GetTemp()
{
    MyClass _A;
    //使用移动语义,阻止NRVO;尝试使用移动构造(已取消)未果,转而尝试调用拷贝构造
	return std::move(_A);
}

int main()
{
	MyClass B = GetTemp();
	//CopyConstructor
}
  • 在此基础上我们若再取消移动构造函数,则会导致程序报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <utility>

class MyClass
{
public:
	//默认构造函数
	MyClass() = default;

	//拷贝构造函数
	MyClass(const MyClass& _other) = delete;

	//声明析构函数,导致禁止编译器实现默认移动构造函数
	~MyClass() {}
};

MyClass GetTemp()
{
    MyClass _A;
	//使用移动语义,阻止NRVO,尝试调用移动与拷贝构造(已取消)未果
	return std::move(_A);
}

int main()
{
	MyClass B = GetTemp();
	//报错:use of deleted function 'MyClass::MyClass(const MyClass&)'gcc
}
  • 所以除非有特殊需求,否则尽量不要对返回值使用std::move,以免阻止编译器的优化机会

六、完美转发

  • C++11提供移动语义后,可以在模板中将参数完美地转发(即不仅能准确转发参数的值,还能保证其左右值属性不变)给其它函数,以解决”右值作为实参传入后变成左值,导致在函数中无法再次以右值传入别的函数”的问题
    • 第一步:将模板函数(或模板类的成员函数)接收的参数书写为T&&形式,这能使得函数既可接收左值引用,又可以接受右值引用
    • 第二步:使用std::forward<T>()函数转发参数到其他函数,若参数是右值(左值),则转发之后仍是右值引用(左值引用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <utility>

//若参数是左值,调用此函数
void Meta(int& _i)
{
	std::cout << "ParameterLeftValue=" << _i << "\n";
}

//若参数是右值,调用此函数
void Meta(int&& _i)
{
	std::cout << "ParameterRightValue=" << _i << "\n";
}

//完美转发的模板函数
template<typename A>
//参数类型为A&&
void Func(A&& _i)
{
	//使用std::forward转发
    Meta(std::forward<A>(_i));
}
 
int main()
{
	int i = 3;
	//实参是左值
	Func(i);
	//实参是右值
	Func(8);
	//传递移动的对象
	Func(std::move(i));

	//ParameterLeftValue=3
	//ParameterRightValue=8
	//ParameterRightValue=3
}
本文由作者按照 CC BY-NC-SA 4.0 进行授权