参考文章

前言

首先有那么几个问题:从 C++98 升级到 C++11 能提升性能吗?从函数中返回 STL 容器的开销大吗?return std::move(x) 有意义吗?
这些问题都牵扯到了移动语义和复制构造。

解释

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
#include <iostream>
#include <string>

struct X {
X() { puts("X()"); }
X(const X&) { puts("X(const X&)"); }
X(X&&)noexcept { puts("X(X&&)"); }
~X() { puts("~X()"); }
};

class Test {
X* m_p;
public:
Test() :m_p{ nullptr } {}
Test(X* x) :m_p{ x } {}
Test(const Test& t) {//单纯的拷贝新的对象。
m_p = new X(*t.m_p);
}
Test(Test&& t)noexcept {//转移所有权,即转移拥有的指向实际数据的指针,无拷贝
m_p = t.m_p;
t.m_p = nullptr;
}
~Test() {
if (m_p != nullptr) {//为空代表无所有权,也不需要delete
delete m_p;
}
}
constexpr bool empty()const noexcept{
return m_p == nullptr;
}
};

Test func() {
Test t{ new X };
puts("---------");
return t;
}

int main() {
{
Test t{ new X };
std::cout << t.empty() << '\n';//打印0
puts("---------");
Test t2{ std::move(t) }; //移动构造 转移所有权
std::cout << t.empty() << '\n';//打印1,表示所有权已经被转移,即t不再拥有指向实际数据X的指针
}
puts("---------------------");
{
Test t{ new X };
std::cout << t.empty() << '\n';//打印0
puts("---------");
Test t2{ t }; //拷贝构造
std::cout << t.empty() << '\n';//打印0
}
puts("----------------------");
{
auto result = func();
}
}

分析这段代码:

  1. X 类型,它是一个空类,拥有的这些函数也只是方便我们观察,假设它是一个用来扮演我们平时的智能指针或者说是 std::vector 之类的容器。
  2. Test 这个类型用来扮演正常的,支持 C++11 移动语义,遵守所有权转移这个君子协议的类类型(如 STL 容器,智能指针)。
  3. func 函数,是否拷贝自己保有的 X,以及为什么。
  4. main 函数用花括号分出了三个作用域,也就代表了三个例子。
  • 第一个作用域打印出的结果是:
    1
    2
    3
    4
    5
    X()
    0
    ---------
    1
    ~X()
    Test t{new X};首先 new X,申请了构造X,调用 X 的构造函数,打印了 X() 。返回了一个指针,调用了 Test 的有参构造,用来初始化它的数据成员 m_p。t 拥有了 X 的所有权。t.empty() 的结果自然为 0,非空。

打印了一个分割线。Test t2{ std::move(t)}; std::move(t)是一个亡值表达式,调用t2的移动构造。

即:将t1的m_p指针赋值给t2,并给t1的m_p赋空。完成了所有权的转移,t1不再拥有X的所有权,t2拥有了X的所有权

所以后面的t.empty()会打印1,因为此时t的m_p已经为空了,m_p == nullptr,理所应当。


  • 第二个作用域打印的结果:
    1
    2
    3
    4
    5
    6
    7
    X()
    0
    ---------
    X(const X&)
    0
    ~X()
    ~X()
    前两行和第一个作用域的一样,Test t2{ t }; 这里调用 Test 的复制构造。即:用 t 的 m_p 去用做 t2.m_p = new X(*t.m_p) 相当于调用 X 的复制构造进行初始化 。

与移动构造不一样,并不是直接转移指针,而是实实在在的 new 构造对象。t2 和 t1 拥有的X对象是一样的,但是不是同一个(因为是用了t的进行初始化,但是我们这是空类,不用在意,演示一下而已)。

t.empty() 的结果为 0,因为 t 并没有被转移所有权,t 依然拥有 X。 最终打印了两个析构,因为 t1 和 t2 各拥有一个 X

移动语义的诞生,就是为了方便区分,到底是需要移动还是真的需要拷贝。


  • 第三个作用域打印的结果:
    1
    2
    3
    X()
    ---------
    ~X()
    我们拿STL的容器vector进行对照展示,方便理解。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <iostream>
    #include <vector>

    struct X {
    X() { puts("X()"); }
    X(const X&) { puts("X(const X&)"); }
    X(X&&)noexcept { puts("X(X&&)"); }
    ~X() { puts("~X()"); }
    };

    std::vector<X> func(){
    std::vector<X>v{X{}};
    puts("------------");
    return v;
    }

    int main(){
    auto result = func();

    }
    运行结果:
    1
    2
    3
    4
    5
    X()
    X(const X&)
    ~X()
    ------------
    ~X()
    这是设置到C++17的打印结果,如果低一些,分割线之前会有更多的打印。
    这个vector的运行结果和我们的第三个作用域打印的结果,或者说他们的代码有什么相同点吗?
    对,没错,分割线后没有再打印构造函数,代表没有拷贝自己实际存储的元素。上面两段代码的语境下,return都只会调用移动构造,来转移所有权。
    我们回到Test的移动构造的实现,只是转移指针,无拷贝。std::vector同理。 完成了所有权的转移。