在类中实现移动语义的规范方法

时间:2016-01-16 20:45:45

标签: c++ c++11 c++14

我正在寻找一个如何使用移动语义实现派生类和基类的好例子。我对它的思考越多,默认的移动构造函数和赋值移动运算符似乎就越能完成这项工作,因为大多数标准(STL)类型和智能指针都是默认可移动的。

无论如何,如果我们有一个类层次结构,需要一个明确的移动实现,我该怎么做 - 至少作为第一次切割?

在这个例子中,我使用的是一个原始指针,我通常将其包装在std :: unique_ptr中,但我需要一个移动的示例,它不是默认的可移动的。

非常感谢任何帮助。 :)

目前,我做了以下尝试:

struct BlobA
{
    char data[0xaa];
};

struct BlobB
{
    char data[0xbb];
};

//----------------------------------------

class Base
{
public:

    //Default construct the Base class
    //C++11 allows the data members to initialised, where declared. Otherwise you would do it here.
    Base()
    {

    }

    //Define the destructor as virtual to ensure that the derived destructor gets called. (In case it is necessary. It's not in this example but still good practice.)
    virtual ~Base()
    {
        delete m_moveableDataInBase; //this is a contrived example to show non-default moveable data, in practice RAII should be used rather than deletes like this
    }

    //Copy constructor, needs to accept a const ref to another Base class with which it copies the data members
    Base(const Base& other) :
    m_moveableDataInBase(new BlobA(*other.m_moveableDataInBase)), // copy the other's moveable data
    m_pNonMoveableDataInBase(other.m_pNonMoveableDataInBase)        // copy the other's non-moveable data
    {

    }

    //Assignment operator uses the canonical copy then swap idiom. It returns a reference to allow chaining: a = b = c
    //This is thus implemented in terms of the copy constructor.
    Base& operator=(const Base& rhs)
    {
        Base temp(rhs);
        Swap(temp);
        return *this;
    }

    //The move construtor is declared as throwing no exceptions so that it will be called by STL algorithms
    //It accepts an rvalue and moves the Base part.
    Base(Base&& other) noexcept :
    m_moveableDataInBase(nullptr) // don't bother allocating our own resources to moveable data because we are about to move (steal) the other's resource
    {
        Swap(other);
    }

    //The move assignment operator is declared as throwing no exceptions so that it will be called by STL algorithms
    //It accepts an rvalue and moves (steals) the data resources from the rhs using swap and copies the non moveable data from the rhs
    Base& operator=(Base&& rhs) noexcept
    {
        //move (steal) the moveable contents from rhs
        std::swap(m_moveableDataInBase, rhs.m_moveableDataInBase);

        //copy the non-moveable contents from rhs
        m_pNonMoveableDataInBase = rhs.m_pNonMoveableDataInBase;

        return *this;
    }

private:
    //this private member swaps the data members' contents.
    //It is private because it isn't virtual and only swaps the base contents and is thus not safe as a public interface
    void Swap(Base& other)
    {
        std::swap(m_moveableDataInBase, other.m_moveableDataInBase);
        std::swap(m_pNonMoveableDataInBase, other.m_pNonMoveableDataInBase);
    }

    //an example of some large blob of data which we would like to move instead of copy for performance reasons.
    //normally, I would have used a unique_ptr but this is default moveable and I need an example of something that isn't
    BlobA* m_moveableDataInBase{ new BlobA };

    //an example of some data that we can't or don't want to move
    int m_pNonMoveableDataInBase = 123;
};

//----------------------------------------

class Derived : public Base
{
public:

    //Default construct the Derived class, this is called after the base class constructor
    //C++11 allows the data members to initialised, where declared. Otherwise you would do it here.
    Derived()
    {

    }

    //Default virtual destructor, to clean up stuff that can't be done automatically through RAII
    virtual ~Derived()
    {
        delete m_pMoveableDataInDerived; //this is a contrived example to show non-default moveable data, in practice RAII should be used rather than deletes like this
    }

    //Copy constructor, needs to accept a const ref to another derived class with which it
    //first copy constructs the base and then copies the derived data members
    Derived(const Derived& other) :
    Base(other),  // forward to the base copy constructor
    m_pMoveableDataInDerived(new BlobB(*other.m_pMoveableDataInDerived)),  // copy the other's moveable data
    m_pNonMoveableDataInDerived(other.m_pNonMoveableDataInDerived)         // copy the other's non-moveable data
    {

    }

    //Assignment operator uses the canonical copy then swap idiom. It returns a reference to allow chaining: a = b = c
    //Because it uses the derived copy constructor, which in turn copy constructs the base, we don't forward to the base assignment operator.
    Derived& operator=(const Derived& rhs)
    {
        Derived temp(rhs);
        Swap(temp);
        return *this;
    }

    //The move construtor is declared as throwing no eceptions so that it will be called by STL algorithms
    //It accepts an rvalue and first moves the Base part and then the Derived part.
    //There is no point in allocating any resource before moving so in this example, m_pBlobB is set to nullptr
    Derived(Derived&& other) noexcept
    : Base(std::move(other)), // forward to base move constructor
    m_pMoveableDataInDerived(nullptr) // don't bother allocating our own resources to moveable data because we are about to move (steal) the other's resource
    {
        Swap(other);
    }

    //The move assignment operator is declared as throwing no exceptions so that it will be called by STL algorithms
    //It accepts an rvalue and first calls the base assignment operator and then moves the data resources from the rhs using swap
    Derived& operator=(Derived&& rhs) noexcept
    {
        //forward to the base move operator=
        Base::operator=(std::move(rhs));

        //move (steal) the moveable contents from rhs
        std::swap(m_pMoveableDataInDerived, rhs.m_pMoveableDataInDerived);

        //copy the non-moveable contents from rhs
        m_pNonMoveableDataInDerived = rhs.m_pNonMoveableDataInDerived;
    }

private:
    //this member swaps the Derived data members contents.
    //It is private because it doesn't swap the base contents and is thus not safe as a public interface
    void Swap(Derived& other) noexcept
    {
        std::swap(m_pMoveableDataInDerived, other.m_pMoveableDataInDerived);
        std::swap(m_pNonMoveableDataInDerived, other.m_pNonMoveableDataInDerived);
    }

    //an example of some large blob of data which we would like to move instead of copy for performance reasons.
    //normally, I would have used a unique_ptr but this is default moveable and I need an example of something that isn't
    BlobB* m_pMoveableDataInDerived{ new BlobB };

    //an example of some data that we can't or don't want to move
    int m_pNonMoveableDataInDerived = 456;
};

3 个答案:

答案 0 :(得分:4)

你必须开始知道你的类不变量是什么。类不变量是在数据成员中始终为真的某种或某种关系。然后,您需要确保您的特殊成员可以使用满足您的类不变量的任何值进行操作。特殊成员不应该有前提条件(除了所有类不变量都必须为真)。

让我们以你的例子为例讨论。首先让我们专注于Base。我喜欢把我的私人数据成员放在前面,以便他们接近特殊成员。通过这种方式,我可以更容易地看到默认或隐含声明特殊成员的实际行为。

<强>基

class Base
{
    //an example of some large blob of data which we would like to move 
    //  instead of copy for performance reasons.
    //normally, I would have used a unique_ptr but this is default moveable
    //   and I need an example of something that isn't
    BlobA* m_moveableDataInBase{ new BlobA };

    //an example of some data that we can't or don't want to move
    int m_pNonMoveableDataInBase = 123;

到目前为止一直很好,但这里有一个轻微的歧义:m_moveableDataInBase == nullptr可以吗?没有一个对或错的答案。这是Base的作者必须回答的问题,然后编写代码来强制执行。

另外,概述您的会员功能。即使您决定要内联它们,也请在声明之外进行。否则你的班级声明变得难以阅读:

class Base
{
    BlobA* m_moveableDataInBase{ new BlobA };
    int m_pNonMoveableDataInBase = 123;

public:
    virtual ~Base();
    Base() = default;
    Base(const Base& other);
    Base& operator=(const Base& rhs);
    Base(Base&& other) noexcept;
    Base& operator=(Base&& rhs) noexcept;
};

析构函数是最有说服力的特殊成员。我喜欢先声明/定义它:

Base::~Base()
{
    delete m_moveableDataInBase;
}

这看起来不错。但是,这还没有回答m_moveableDataInBase是否nullptr的问题。接下来,如果它存在,则为默认构造函数。在可行时优先考虑= default定义。

现在是复制构造函数:

Base::Base(const Base& other)
    : m_moveableDataInBase(new BlobA(*other.m_moveableDataInBase))
    , m_pNonMoveableDataInBase(other.m_pNonMoveableDataInBase)
{
}

好的,这说明了一些重要的事情:

other.m_moveableDataInBase != nullptr  // ever

我向前看了一眼,看着你的移动构造函数,然后将移动的值保留为m_moveableDataInBase == nullptr。所以我们遇到了一个问题:

  1. 您的复制构造函数中存在错误,您应该检查案例other.m_moveableDataInBase == nullptr

  2. 您的移动构造函数中存在一个错误,它不应该将移动状态保留为m_moveableDataInBase == nullptr

  3. 两种解决方案都不正确。 Base作者必须做出此设计决定。如果他选择2,那么实际上没有合理的方法来实现移动构造函数,使得它比复制构造函数更快。在这种情况下,要做的事情不是编写移动构造函数,只是让复制构造函数完成工作。所以我选择1以便仍然有一个移动构造函数来讨论。更正了复制构造函数:

    Base::Base(const Base& other)
        : m_moveableDataInBase(other.m_moveableDataInBase ?
                               new BlobA(*other.m_moveableDataInBase) :
                               nullptr)
        , m_pNonMoveableDataInBase(other.m_pNonMoveableDataInBase)
    {
    }
    

    另外,既然我们选择了这个不变量,那么重新访问默认构造函数可能并不是一个坏主意,而是说:

        BlobA* m_moveableDataInBase = nullptr;
    

    现在我们有一个noexcept默认构造函数。

    接下来是复制赋值运算符。不要陷入默认选择复制/交换习语的陷阱。有时这个成语很好。但它往往表现不佳。 And performance is more important than code reuse。考虑复制/交换的替代方法:

    Base&
    Base::operator=(const Base& rhs)
    {
        if (this != &rhs)
        {
            if (m_moveableDataInBase == nullptr)
            {
                if (rhs.m_moveableDataInBase != nullptr)
                    m_moveableDataInBase = new BlobA(*rhs.m_moveableDataInBase);
            }
            else  // m_moveableDataInBase != nullptr
            {
                if (rhs.m_moveableDataInBase != nullptr)
                    *m_moveableDataInBase = *rhs.m_moveableDataInBase;
                else
                {
                    delete m_moveableDataInBase;
                    m_moveableDataInBase = nullptr;
                }
            }
            m_pNonMoveableDataInBase = rhs.m_pNonMoveableDataInBase;
        }
        return *this;
    }
    

    如果Base的值通常为m_moveableDataInBase != nullptr,则此重写速度明显快于复制/交换。在这种常见情况下,复制/交换总是执行1次删除和1次删除。此版本执行0次新闻和0次删除。它只复制170个字节。

    如果我们选择的设计是m_moveableDataInBase != nullptr的不变量,那么复制任务变得更加简单:

    Base&
    Base::operator=(const Base& rhs)
    {
        *m_moveableDataInBase = *rhs.m_moveableDataInBase;
        m_pNonMoveableDataInBase = rhs.m_pNonMoveableDataInBase;
        return *this;
    }
    

    最小化对堆的调用是而不是过早优化。这是工程。这就是移动语义的产生。这正是为什么std::vectorstd::string副本分配不使用复制/交换习惯用法。这太慢了。

    移动构造函数:我会像这样编码:

    Base::Base(Base&& other) noexcept
        : m_moveableDataInBase(std::move(other.m_moveableDataInBase))
        , m_pNonMoveableDataInBase(std::move(other.m_pNonMoveableDataInBase))
    {
        other.m_moveableDataInBase = nullptr;
    }
    

    这节省了一些装载和存储。我没有费心检查生成的组件。我建议你在选择这个实施之前这样做。在noexcept移动构造函数中,计算加载和存储。

    作为一个风格指南,我喜欢move成员,即使我知道他们是标量并且移动没有影响。这使得代码的读者不必确保所有未移动的成员都是标量。

    您的搬家任务对我来说很合适:

    Base&
    Base::operator=(Base&& rhs) noexcept
    {
        //move (steal) the moveable contents from rhs
        std::swap(m_moveableDataInBase, rhs.m_moveableDataInBase);
        //copy the non-moveable contents from rhs
        m_pNonMoveableDataInBase = rhs.m_pNonMoveableDataInBase;
        return *this;
    }
    

    有一次当你不想这样做的时候,你需要立即销毁的lh上有非内存资源,而不是交换到rhs。但是你的例子只是交换内存。

    <强>派生

    Derived对于Base,我会完全按照Base显示的方式编写,除非您首先在代码中完全复制/移动Derived::Derived(Derived&& other) noexcept : Base(std::move(other)) , m_pMoveableDataInDerived(std::move(other.m_pMoveableDataInDerived)) , m_pNonMoveableDataInDerived(std::move(other.m_pNonMoveableDataInDerived)) { other.m_pMoveableDataInDerived = nullptr; } 。例如,这里是移动构造函数:

    ~Dervied()

    同时使用override代替virtual标记~Base()。您希望编译器告诉您是否意外地以~Derived()覆盖了class Derived : public Base { BlobB* m_pMoveableDataInDerived = nullptr; int m_pNonMoveableDataInDerived = 456; public: ~Derived() override; Derived() = default; Derived(const Derived& other); Derived& operator=(const Derived& rhs); Derived(Derived&& other) noexcept; Derived& operator=(Derived&& rhs) noexcept; };

    static_assert

    <强>测试

    同时使用static_assert(std::is_nothrow_destructible<Base>{}, ""); static_assert(std::is_nothrow_default_constructible<Base>{}, ""); static_assert(std::is_copy_constructible<Base>{}, ""); static_assert(std::is_copy_assignable<Base>{}, ""); static_assert(std::is_nothrow_move_constructible<Base>{}, ""); static_assert(std::is_nothrow_move_assignable<Base>{}, ""); static_assert(std::is_nothrow_destructible<Derived>{}, ""); static_assert(std::is_nothrow_default_constructible<Derived>{}, ""); static_assert(std::is_copy_constructible<Derived>{}, ""); static_assert(std::is_copy_assignable<Derived>{}, ""); static_assert(std::is_nothrow_move_constructible<Derived>{}, ""); static_assert(std::is_nothrow_move_assignable<Derived>{}, ""); 和类型特征测试所有六个特殊成员(无论您是否拥有):

    Blob

    您甚至可以针对static_assert(std::is_trivially_destructible<BlobA>{}, ""); static_assert(std::is_trivially_default_constructible<BlobA>{}, ""); static_assert(std::is_trivially_copy_constructible<BlobA>{}, ""); static_assert(std::is_trivially_copy_assignable<BlobA>{}, ""); static_assert(std::is_trivially_move_constructible<BlobA>{}, ""); static_assert(std::is_trivially_move_assignable<BlobA>{}, ""); static_assert(std::is_trivially_destructible<BlobB>{}, ""); static_assert(std::is_trivially_default_constructible<BlobB>{}, ""); static_assert(std::is_trivially_copy_constructible<BlobB>{}, ""); static_assert(std::is_trivially_copy_assignable<BlobB>{}, ""); static_assert(std::is_trivially_move_constructible<BlobB>{}, ""); static_assert(std::is_trivially_move_assignable<BlobB>{}, ""); 类型测试这些内容:

    noexcept

    <强>摘要

    总之,给予六个特殊成员他们应得的所有爱护,即使结果是禁止它们,隐式声明它们,或明确默认或删除它们。编译器生成的移动成员将移动每个基础,然后移动每个非静态数据成员。喜欢这个食谱,在你可以的时候默认它,并在必要时简单地增加它。

    通过将成员函数定义移出类声明来突出显示类API。

    测试。至少测试你是否拥有所有6个特殊成员,如果你拥有它们,如果它们是{{1}}或琐碎(或不是)。

    谨慎使用复制/交换。它可能是一个性能杀手。

答案 1 :(得分:3)

  

无论如何,在我们有一个类层次结构的情况下,它需要一个   明确的移动实现,我该怎么做 - 至少作为第一个   切断

唐&#39;吨

你没有移动基类。您将指针移动到基类。对于派生类,您可以移动它,但随后您知道派生类是什么,因此您可以相应地编写移动构造函数/赋值运算符。

此外,原始指针是完全可移动的。您如何看待unique_ptr的实施?

答案 2 :(得分:0)

我会抛弃“与其他人交换”的做法,这种做法对可读性没有帮助,而是采用简单的作业。

class A{
   int * dataA;
   public:
   A() { dataA = new int(); }
   virtual ~A() {delete dataA; }
   A(A&& rhs) noexcept { dataA = rhs.dataA; rhs.dataA = nullptr; } 
   A& operator = (A&& rhs) noexcept {
      if (this != &rhs){
       if (dataA) delete  dataA; 
       dataA = rhs.dataA;
       rhs.dataA = nullptr;
      }
      return *this;
   } 
}

class B: public A{
   int* dataB;
   public:    
       B(){ dataB = new int(); }
       virtual ~B() {delete dataB; }
       B(B&& rhs) noexcept : A(std::move(rhs)) { dataB = rhs.dataB; rhs.dataB = nullptr; }  

   B& operator = (B&& rhs) noexcept {
      A::operator == (std::move(rhs));
      if (this != &rhs){           
      if (dataB) delete  dataB; 
      dataB = rhs.dataB;
      rhs.dataB = nullptr;
      }
      return *this;
   }
}

调用父移动构造函数来移动对象的父部分。在移动构造函数中完成剩下的工作。

对于赋值运算符也一样。