什么情况下 C++ 会调用复制构造、移动构造和赋值运算

为什么要把复制构造、移动构造和赋值运算三个放在一起说呢?因为他们都和一个运算符 `=` 有关系。虽然写起来都是 `=` ,但是表示的含义可能完全不同。

但万变不离其宗, 复制构造、移动构造归根结底是构造函数,用于构造类的实例的。而赋值运算本质上是一个运算符,是操作类实例的。

# 复制构造

即使是最基础的C++教程,也会把复制构造函数作为类设计的重点之一。C++虽然会为类创建默认的复制构造函数,但是默认的复制构造函数只提供最基础的功能,也就是类成员间的值拷贝,所以遇到裸指针等掌握了独有资源的类来说就会非常危险了。此时就需要创建复制构造函数。

```cpp
class MyClass
{
    MyClass(const MyClass& instance) {}
}
```

这里有一个要点:**参数类型必须是自身类型的常量引用**。

那么复制构造函数什么时候会被用到呢?有以下几种情况

一、通过类的另一个实例构造类实例

```cpp
MyClass a {};
MyClass b = a; // 调用复制构造函数
```

我们构造了一个类实例 `b` ,但是其初始值是 `a` ,此时调用复制构造函数。

二、通过值传递参数

```cpp
void my_function(MyClass obj);
my_function(a);
```

`a` 在传递给 `obj` 时会被复制一份。

当然还有其他情况。但有一种情况是例外的,看似在复制构造,实际上可能并没有复制。

```cpp
MyClass create()
{
    MyClass a;
    // 对 a 进行一些操作
    return a;
}
```

貌似 `a` 返回后要被复制一份,但是由于编译器的优化,这里 `a` 直接在赋值位置被构造,避免了复制一次。

# 移动构造

移动构造主要是针对提升性能和右值引用设计的。关于左值引用和右值引用的讨论很多,细节也很多,不过大体上而言,对临时的、不能取地址的、没有名字的变量只能取右值引用,或者说,只能出现在等号右边不能出现在等号左边的就是右值。右值引用用 `&&` 表示。但是一个非临时的、能取地址的、有名字的变量,可以使用 `std::move()` 函数将其变为右值引用。

移动构造函数就是参数为**自身类型右值引用**的构造函数,

```cpp
class MyClass
{
    MyClass(MyClass&& instance) {}
}
```

注意不能是常量右值引用,因为新实例夺取了原实例的资源,所以往往需要对原实例进行一些修改,避免原实例再一次释放资源。

那么移动构造函数的使用场景是什么?

一、使用 `std::move()` 将原实例移动到新实例,从而避免复制

最常见的是智能指针 `unique_ptr<>` 的操作。由于该只能指针的所有权是唯一的,只能有一个实例控制该指针,所以只能进行移动操作。

二、函数参数使用右值引用以接受临时变量或避免拷贝

一种情况是操作容器类型时,例如 `vector<>`,可以使用 `emplace_back()` 插入元素,避免对象复制。

还有一种情况是做矩阵运算等复杂运算。比如有两个矩阵 `A` 和 `B` ,现在要求 `A` 左乘以 `B` 的转置,也就是

```cpp
A * t(B)
```

这里的 `*` 一定是重载的乘法运算符,以支持矩阵运算。而 `t()` 函数返回的是一个临时变量,因此只能让重载的 `*` 运算符支持右值引用,从而实现上述表达式。不然就得将 `t(B)` 保存成 `Bt` 然后在计算 `A*Bt` 。这是非常不方便的。

然而如果经过编译器优化,如果函数参数传入的右值引用是构造函数构造的,那么往往移动构造函数都会避免。比如

```cpp
void print(MyClass&& instance);
print(create())
```

虽然 `create()` 函数构造了一个对象,以右值引用的形式传入了 `print`,看似这里需要移动构造,但实际上并没有。因此,返回新创建的类对象的函数,如 `create()` ,无需将返回值类型设置为右值引用,这样反而会干扰编译器的优化。

由此可见,移动构造函数的使用场景还是比较有限的,一般只有用到 `unique_ptr<>` 智能指针和避免深拷贝提高性能的时候需要用到。

# 赋值运算

这个相对来说就简单多了,重载 `operator=` 即可。但是传入左值引用和右值引用的情况需要分别处理。往往如果是左值,需要执行类似于复制的逻辑。

```cpp
class MyClass {
    // 普通赋值运算符的重载
    MyClass& operator=(const MyClass& other) {
        if (this != &other) { // 避免自我赋值
            data = other.data;
        }
        return *this;
    }
};

```

如果传入的是函数返回值,上述拷贝赋值依然是可以执行的。但是这就会多出一次复制的成本。如果要支持移动,需要单独设计移动逻辑。

```cpp
MyClass& operator=(MyClass&& other) noexcept {
    if (this != &other) { // 避免自我赋值
        data = std::move(other.data);
    }
    return *this;
}

```

但是需要注意的是,只有当类对象构造好了之后才能进行赋值,否则执行的还是构造函数。

# 实例

下面是的类型设计了复制构造、移动构造、赋值运算。

```cpp
#include <iostream>

using namespace std;

class MyClass
{
public:
    static size_t Total;
    static const std::string Identifer;

    MyClass(size_t size): mSize(size), mId(Total++)
    {
        mpData = new int[mSize];
        std::cout << "[MyClass] " << Identifer.data()[mId] << " construct\n";
    }

    MyClass(const MyClass& instance)
    {
        mId = Total++;
        mSize = instance.mSize;
        mpData = new int[mSize];
        memcpy(mpData, instance.mpData, mSize * sizeof(int));
        std::cout << "[MyClass] " << Identifer.data()[mId] << " copy construct from " << Identifer.data()[instance.mId] << "\n";
    }

    MyClass(MyClass&& instance)
    {
        mId = instance.mId;
        mSize = instance.mSize;
        mpData = instance.mpData;
        instance.mMoved = true;
        std::cout << "[MyClass] " << Identifer.data()[mId] << " move construct\n";
    }

    ~MyClass()
    {
        if (!mMoved) delete[] mpData;
        mpData = nullptr;
        mSize = 0;
        Total--;
        if (mMoved)
            std::cout << "[MyClass] " << Identifer.data()[mId] << " destroy skiped\n";
        else
            std::cout << "[MyClass] " << Identifer.data()[mId] << " destroyed\n";
    }

    const int* data() { return mpData; }

    const size_t size() { return mSize; }

    void set(size_t i, int value) { mpData[i] = value; }

    MyClass& operator=(const MyClass& other)
    {
        std::cout << "[MyClass] " << Identifer.data()[mId] << " copy assigned from " << Identifer.data()[other.mId] << "\n";
        if (this != &other)
        {
            delete[] mpData;
            mId = other.mId;
            mSize = other.mSize;
            mpData = new int[mSize];
            memcpy(mpData, other.mpData, mSize * sizeof(int));
        }
        return *this;
    }

    MyClass& operator=(MyClass&& other)
    {
        std::cout << "[MyClass] " << Identifer.data()[mId] << " move assigned from " << Identifer.data()[other.mId] << "\n";
        if (this != &other)
        {
            delete[] mpData;
            mId = other.mId;
            mSize = other.mSize;
            mpData = other.mpData;
            other.mMoved = true;
        }
        return *this;
    }

private:
    int mId;
    int* mpData;
    size_t mSize;
    bool mMoved = false;
};

inline size_t MyClass::Total = 0;
inline const std::string MyClass::Identifer = "abcdefghijklmnopqrstuvwxyz";

void print(MyClass obj)
{
    cout << "[print] begin\n";
    for (size_t i = 0; i < obj.size(); i++)
    {
        cout << (i == 0 ? "" : ", ") << obj.data()[i];
    }
    cout << "\n";
    cout << "[print] end\n";
}

void print_ref(MyClass& obj)
{
    cout << "[print] begin\n";
    for (size_t i = 0; i < obj.size(); i++)
    {
        cout << (i == 0 ? "" : ", ") << obj.data()[i];
    }
    cout << "\n";
    cout << "[print] end\n";
}

void print_move(MyClass&& obj)
{
    cout << "[print] begin\n";
    for (size_t i = 0; i < obj.size(); i++)
    {
        cout << (i == 0 ? "" : ", ") << obj.data()[i];
    }
    cout << "\n";
    cout << "[print] end\n";
}

MyClass my_create(initializer_list<int> elements)
{
    cout << "[my_create] begin\n";
    MyClass c(elements.size());
    auto cursor = elements.begin();
    for (size_t i = 0; i < elements.size(); i++)
    {
        c.set(i, cursor[i]);
    }
    cout << "[my_create] end\n";
    return c;
}

int main(int, char**){
    MyClass a(10);
    MyClass b = a;
    print(b);
    print_ref(b);
    MyClass c = my_create({1, 2, 3});
    print_ref(c);
    print_move(my_create({4, 5, 6}));
    cout << "[main] move construct\n";
    MyClass c1(std::move(c));
    print_ref(c1);
    cout << "[main] assign\n";
    c1 = a;
    c1 = my_create({7, 8, 9});
    cout << "[main] cleaning up\n";
}

```

那么 `main()` 函数执行的时候,到底进行了多少次构造、多少次复制构造、多少次拷贝构造?

请看程序运行结果吧。

```plaintext
[MyClass] a construct
[MyClass] b copy construct from a
[MyClass] c copy construct from b
[print] begin
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
[print] end
[MyClass] c destroyed
[print] begin
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
[print] end
[my_create] begin
[MyClass] c construct
[my_create] end
[print] begin
1, 2, 3
[print] end
[my_create] begin
[MyClass] d construct
[my_create] end
[print] begin
4, 5, 6
[print] end
[MyClass] d destroyed
[main] move construct
[MyClass] c move construct
[print] begin
1, 2, 3
[print] end
[main] assign
[MyClass] c copy assigned from a
[my_create] begin
[MyClass] d construct
[my_create] end
[MyClass] a move assigned from d
[MyClass] d destroy skiped
[main] cleaning up
[MyClass] d destroyed
[MyClass] c destroy skiped
[MyClass] b destroyed
[MyClass] a destroyed
```