移动构造和移动赋值

发布于 2023-06-04 23:03:15 字数 8420 浏览 47 评论 0

前面已经讨论过通过重载拷贝构造函数和赋值运算符来实现 move 导致的一系列问题。现在我们来看看 C++ 11 如何通过移动构造和移动赋值来解决这些问题。

1. 拷贝构造函数和拷贝赋值

首先来回顾一下 copy 语义。拷贝构造函数通过拷贝已经存在的对象来初始化一个新的对象。copy 赋值用于将一个对象拷贝给另外一个对象。如果没有定义,c++编译器会给一个默认的拷贝构造函数和拷贝赋值函数,这两个函数仅做浅拷贝,所以对于含有动态申请内存的类来说,这两个默认函数存在一些问题,因此,处理带有动态内存的类必须要重写这两个函数做深拷贝。 在之前的文章中,我们已经实现过两个版本的Auto_ptr,现在来看看第三个版本

template<class T>
class Auto_ptr3
{
    T* m_ptr;
public:
    Auto_ptr3(T* ptr = nullptr)
        :m_ptr(ptr)
    {
    }

    ~Auto_ptr3()
    {
        delete m_ptr;
    }

    // Copy constructor
    // Do deep copy of a.m_ptr to m_ptr
    Auto_ptr3(const Auto_ptr3& a)
    {
        m_ptr = new T;
        *m_ptr = *a.m_ptr;
    }

    // Copy assignment
    // Do deep copy of a.m_ptr to m_ptr
    Auto_ptr3& operator=(const Auto_ptr3& a)
    {
        // Self-assignment detection
        if (&a == this)
            return *this;

        // Release any resource we're holding
        delete m_ptr;

        // Copy the resource
        m_ptr = new T;
        *m_ptr = *a.m_ptr;

        return *this;
    }

    T& operator*() const { return *m_ptr; }
    T* operator->() const { return m_ptr; }
    bool isNull() const { return m_ptr == nullptr; }
};

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

Auto_ptr3<Resource> generateResource()
{
    Auto_ptr3<Resource> res(new Resource);
    return res; // this return value will invoke the copy constructor
}

int main()
{
    Auto_ptr3<Resource> mainres;
    mainres = generateResource(); // this assignment will invoke the copy assignment

    return 0;
}

在这个例子中,我们在main函数中调用generateResource函数得到一个智能指针,接着赋值给一个已经存在的智能指针mainres。这个程序的运行结果是:

Resource acquired
Resource acquired
Resource destroyed
Resource acquired
Resource destroyed
Resource destroyed

(如果编译器做了返回值优化(RVO)的话,也可能仅有四条输出)。 短短两行代码,却有这么多次资源创建和销毁,是怎么回事?我们来具体看一下到底发生了什么!

  1. 在generateResource函数中,局部变量res被创建出来,并用动态申请的Resource来初始化,因此打印出第一行Resource acquired。
  2. 局部变量res通过传值返回给main函数,这里通过调用拷贝构造函数将res拷贝到一个临时对象,因为我们的拷贝是深拷贝,因此会再次创建一个Resource,所以会打印出第二次 Resource acquired。
  3. generateResource函数返回,此时res出作用域被销毁,因此打印出第一个Resource destroyed。
  4. 刚才产生的临时对象被拷贝到mainres,这是通过拷贝赋值函数实现的,拷贝赋值也是深拷贝,因此会再次创建一个Resource,所以会打印出第三个Resource acquired。
  5. 拷贝赋值完成后,临时对象出作用域被销毁,因此打印出第二个Resource destroyed。
  6. main函数结束后,mailres出作用域被销毁,因此打印出第三个Resource destroyed。

可以看出来,因为我们调用了一次拷贝构造函数和一次拷贝赋值函数,因此多出来两次不必要的Resource构造和销毁。效率很低,但是至少没有crash。现在,有了c++11的move semantics,我们可以做的更高效。

2. 移动构造函数和移动赋值

C++11定义了两个新的函数以实现move semantics,分别是移动构造函数和移动赋值函数。不同于拷贝构造函数和拷贝赋值将一个对象拷贝到另外一个对象,移动构造函数和移动赋值函数将对象的ownership从一个对象移动到另一个对象,显然,移动的开销要比拷贝的开销小很多。 再来看拥有移动构造函数和移动赋值函数的auto_ptr版本。

#include <iostream>

template<class T>
class Auto_ptr4
{
    T* m_ptr;
public:
    Auto_ptr4(T* ptr = nullptr)
        :m_ptr(ptr)
    {
    }

    ~Auto_ptr4()
    {
        delete m_ptr;
    }

    // Copy constructor
    // Do deep copy of a.m_ptr to m_ptr
    Auto_ptr4(const Auto_ptr4& a)
    {
        m_ptr = new T;
        *m_ptr = *a.m_ptr;
    }

    // Move constructor
    // Transfer ownership of a.m_ptr to m_ptr
    Auto_ptr4(Auto_ptr4&& a) noexcept
        : m_ptr(a.m_ptr)
    {
        a.m_ptr = nullptr; // we'll talk more about this line below
    }

    // Copy assignment
    // Do deep copy of a.m_ptr to m_ptr
    Auto_ptr4& operator=(const Auto_ptr4& a)
    {
        // Self-assignment detection
        if (&a == this)
            return *this;

        // Release any resource we're holding
        delete m_ptr;

        // Copy the resource
        m_ptr = new T;
        *m_ptr = *a.m_ptr;

        return *this;
    }

    // Move assignment
    // Transfer ownership of a.m_ptr to m_ptr
    Auto_ptr4& operator=(Auto_ptr4&& a) noexcept
    {
        // Self-assignment detection
        if (&a == this)
            return *this;

        // Release any resource we're holding
        delete m_ptr;

        // Transfer ownership of a.m_ptr to m_ptr
        m_ptr = a.m_ptr;
        a.m_ptr = nullptr; // we'll talk more about this line below

        return *this;
    }

    T& operator*() const { return *m_ptr; }
    T* operator->() const { return m_ptr; }
    bool isNull() const { return m_ptr == nullptr; }
};

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

Auto_ptr4<Resource> generateResource()
{
    Auto_ptr4<Resource> res(new Resource);
    return res; // this return value will invoke the move constructor
}

int main()
{
    Auto_ptr4<Resource> mainres;
    mainres = generateResource(); // this assignment will invoke the move assignment

    return 0;
}

移动构造和移动赋值是很简单的,我们这是简单的把新对象的指针指向了原有对象的资源,然后将原有对象的指针置为空, 这样就把资源从原有对象“移动”到了新对象。这段代码的执行结果是:

Resource acquired
Resource destoryed

看,这样好多了。这段代码的执行流程跟上面那个例子一样,只是在调用拷贝构造函数和拷贝赋值函数的时候,分别调用了移动构造函数和移动赋值函数。

  1. 在generateResource函数中,局部变量res被创建出来,并用动态申请的Resource来初始化,因此打印出第一行Resource acquired。
  2. 局部变量res通过传值返回给main函数,这里通过调用移动构造函数将res移动到一个临时对象。
  3. generateResource函数返回,此时res出作用域被销毁,因为此时res是一个空指针,因此什么也没有发生。
  4. 刚才产生的临时对象被移动到mainres,这是通过移动赋值函数实现的。
  5. 移动赋值完成后,临时对象出作用域被销毁,此时临时对象的指针为空,因此什么也没发生。
  6. main函数结束后,mailres出作用域被销毁,因此打印出第三个Resource destroyed。

这段代码仅有一次资源的构造和析构,但是有两次move。

3. 什么时候会调用移动构造和移动赋值呢?

如果一个类定义类移动构造函数和移动赋值函数,并且传参传的是右值的时候,会调用移动构造或移动赋值函数。一般情况下,这个右值要么是字面量,要么是临时对象。 在大多数情况下,编译器不会提供默认的移动构造和移动赋值函数,除非这个类没有定义拷贝构造函数,拷贝赋值函数,移动构造函数,移动赋值函数和析构函数。 而且,编译器提供的移动构造和移动赋值函数跟默认的拷贝构造,拷贝赋值函数做的事情一样。

Rule: 如果需要移动构造和移动赋值,你必须自己写。

4. move semantics 的核心思想

如果我们在构造或者赋值时,传入的参数是一个左值,我们只能做copy,不能move,因为左值在后面还有可能会用到,我们不能认为修改左值是安全的。比如a=b这行代码,我们不能指望b能被改变。

如果调用构造或者赋值时,传入的参数是右值,我们知道右值只是某种临时对象,我们可以放心的move而不是copy。这样做是安全的,因为右值在语句结束就会被销毁,我们不可能在接下里的代码中继续使用它。

C++11通过右值引用给我们提供了根据不同参数(左值还是右值)调用不同构造或赋值函数的能力,让代码更高效。

5. move 函数应该让两边的对象都处于定义良好的状态

在上面的例子中,移动构造和移动赋值函数最后都将原有对象的指针置为nullptr了,这看起来是不必要的,毕竟如果传入的对象是一个右值,那么在语句结束的时候总是要被销毁的,为什么我们要多此一举在函数里面做清理工作呢?

答案很简单,当传入的右值出了作用域的时候,它的析构函数会被调用,如果它的指针还指向原来申请的内存,那么这块内存会被释放,导致新对象的指针成为野指针。

另外,前面说到传入的参数是右值时,会调用移动构造或移动赋值函数,其实,传入左值也可以选择调用移动构造或移动赋值函数,这个在后面会继续讨论。

6. 函数按值返回的左值可以被 move 而不必 copy

在auto_ptr4版本的generateResource函数,res是通过传值返回的,虽然它是一个左值,但是它调用的是move而不是copy。C++规范中有一个特殊的规定,从函数返回的automatic object如果是通过传值返回的话,可以被move而不需要copy,即便他们是一个左值。这个是合理的,既然generateResource函数中的res在函数结束的时候马上就要被销毁掉,那么我们“偷”一下它的资源也是合理的,这样就避免了昂贵又没有必要的copy开销。

尽管编译器可以move函数返回值,但是在某些case,还可以更进一步,在这些case,不管是move构造函数还是copy构造函数都不会被调用(即返回值优化RVO)。

7. 禁止拷贝

在上面的auto_ptr4版本中,我们保留了拷贝构造函数和拷贝赋值函数是为了和移动构造函数,移动赋值函数做比较。但是有时候我们需要禁止copy,因为copy的开销很大,而且有时候对象T也不支持copy。 下面这个版本的Auto_ptr支持move但是不支持copy。

#include <iostream>

template<class T>
class Auto_ptr5
{
    T* m_ptr;
public:
    Auto_ptr5(T* ptr = nullptr)
        :m_ptr(ptr)
    {
    }

    ~Auto_ptr5()
    {
        delete m_ptr;
    }

    // Copy constructor -- no copying allowed!
    Auto_ptr5(const Auto_ptr5& a) = delete;

    // Move constructor
    // Transfer ownership of a.m_ptr to m_ptr
    Auto_ptr5(Auto_ptr5&& a) noexcept
        : m_ptr(a.m_ptr)
    {
        a.m_ptr = nullptr;
    }

    // Copy assignment -- no copying allowed!
    Auto_ptr5& operator=(const Auto_ptr5& a) = delete;

    // Move assignment
    // Transfer ownership of a.m_ptr to m_ptr
    Auto_ptr5& operator=(Auto_ptr5&& a) noexcept
    {
        // Self-assignment detection
        if (&a == this)
            return *this;

        // Release any resource we're holding
        delete m_ptr;

        // Transfer ownership of a.m_ptr to m_ptr
        m_ptr = a.m_ptr;
        a.m_ptr = nullptr;

        return *this;
    }

    T& operator*() const { return *m_ptr; }
    T* operator->() const { return m_ptr; }
    bool isNull() const { return m_ptr == nullptr; }
};

在这个版本中,如果你尝试将一个Auto_ptr5左值通过值传递给一个函数,编译器会报错拷贝构造函数被删除了。这是很好的,因为我们无论如何都应该通过const左值引用来传递Auto_ptr。

Auto_ptr5 是一个比较不错的智能指针,实际上标准库中的std::unique_ptr跟auto_ptr5非常类似,推荐使用。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文