移动ctor和移动dtor

时间:2010-08-03 16:49:40

标签: c++ c++11 destructor move-semantics move-constructor

正如我在Move constructor/operator=中提出的那样,过了一段时间我已经同意并接受了对这个问题的正确答案,我只是想,如果像“移动析构函数”这样的话会有用吗? 每次我们使用move ctor或operator =时都会在移动的对象上调用  通过这种方式,我们必须仅在移动dtor中指定我们想要的内容以及在移动构造函数使用后我们的对象如何无效。没有这个语义,它看起来每次我写移动ctor或operator =我必须在每个中明确说明(代码重复/错误介绍)如何使移动的对象无效,这不是我认为最好的选择。期待您对此主题的看法。

3 个答案:

答案 0 :(得分:2)

你能带一个具体的例子,它会有用吗?例如,据我所知,移动分配可能在一般情况下实施为

this->swap(rhv); 

如果类受益于移动语义,则交换方法在任何情况下都可能是有益的。这很好地委托了将*this的旧资源发布到常规析构函数的工作。

如果没有显示新类型析构函数的特定示例是实现正确代码的优雅方式,那么您的提案看起来并不吸引人。

另外,根据最新版本,可以默认移动构造函数/赋值运算符。这意味着非常可能 我的课程将如下所示:

class X
{
    well_behaved_raii_objects;
public:
    X(X&& ) = default;
    X& operator=(X&&) = default;
}; 

根本没有析构函数!有什么能让我吸引两个析构函数呢?

还要考虑到赋值运算符具有旧资源来处理。按照当前的标准,你必须要小心正常的析构函数调用在构造和赋值之后都很好,而IMO,与提议的移动析构函数类似,你必须在构造函数和赋值运算符中注意同样的析构函数可以是安全地叫。或者你想要两个移动析构函数 - 每个一个? :)


使用移动构造函数/赋值

在注释中重写了msdn example的示例
#include <algorithm>

class MemoryBlock
{
public:

   // Simple constructor that initializes the resource.
   explicit MemoryBlock(size_t length)
      : length(length)
      , data(new int[length])
   {
   }

   // Destructor.
   ~MemoryBlock()
   {
      delete[] data; //checking for NULL is NOT necessary
   }

   // Copy constructor.
   MemoryBlock(const MemoryBlock& other)
      : length(other.length)
      , data(new int[other.length])
   {
      std::copy(other.data, other.data + length, data);
   }

   // Copy assignment operator (replaced with copy and swap idiom)
   MemoryBlock& operator=(MemoryBlock other)  //1. copy resource
   {
       swap(other);  //2. swap internals with the copy
       return *this; //3. the copy's destructor releases our old resources
   }

   //Move constructor
   //NB! C++0x also allows delegating constructors
   //alternative implementation:
   //delegate initialization to default constructor (if we had one), then swap with argument
   MemoryBlock(MemoryBlock&& other)
    : length(other.length)
    , data(other.data)
    {
        other.data = 0; //now other can be safely destroyed
        other.length = 0; //not really necessary, but let's be nice
    }

    MemoryBlock& operator=(MemoryBlock&& rhv)
    {
        swap(rhv);
        //rhv now contains previous contents of *this, but we don't care:
        //move assignment is supposed to "ruin" the right hand value anyway
        //it doesn't matter how it is "ruined", as long as it is in a valid state
        //not sure if self-assignment can happen here: if it turns out to be possible
        //a check might be necessary, or a different idiom (move-and-swap?!)
        return *this;
    }


   // Retrieves the length of the data resource.
   size_t Length() const
   {
      return length;
   }

   //added swap method (used for assignment, but recommended for such classes anyway)
   void swap(MemoryBlock& other) throw () //swapping a pointer and an int doesn't fail
   {
        std::swap(data, other.data);
        std::swap(length, other.length);
    }

private:
   size_t length; // The length of the resource.
   int* data; // The resource.
};

对原始MSDN示例的一些评论:

1)在delete之前检查NULL是不必要的(可能在这里完成了我已经剥离的输出,也许这表明存在误解)

2)删除赋值运算符中的资源:代码重复。使用复制和交换习语删除以前保存的资源被委托给析构函数。

3)复制和交换习语也不需要自我分配检查。如果在删除之前复制资源,则不会出现问题。 - (另一方面,“无论如何复制资源”只会在您希望使用此类完成大量自我分配时受到伤害。)

4)MSDN示例中的赋值运算符缺少任何异常安全性:如果分配新存储失败,则该类将处于无效状态且指针无效。在破坏时,将发生未定义的行为。

这可以通过仔细重新排序语句,并将删除的指针设置为中间的NULL来改进(不幸的是,这个特定类的不变量似乎总是拥有一个资源,因此让它干净利落地丢失资源例外的情况也不完美)。相比之下,通过复制和交换,如果发生异常,则左侧值保持原始状态(更好的是,操作无法完成,但数据丢失是可以避免的)。

5)自动分配检查在移动赋值运算符中看起来特别有问题。我不认为左手值如何与右手值相同。是否需要a = std::move(a);来实现身份(看起来它仍然是未定义的行为?)?

6)同样,移动分配是不必要地管理资源,我的版本只是委托给常规析构函数。

结论:您看到的代码重复是可以避免的,它只是由一个天真的实现引入(由于某种原因,您可能会在教程中看到,可能是因为重复的代码更易于学习者遵循)。

  

为防止资源泄漏,请始终免费   资源(如内存,文件   移动中的句柄和插槽   赋值运算符。

...如果你重复代码重复,否则重用析构函数。

  

防止不可恢复   适当地破坏资源   处理移动中的自我分配   赋值运算符。

...或确保在确定可以更换之前永远不删除任何内容。 或者更确切地说是一个SO问题:如果在明确定义的程序中进行移动分配,是否可能发生自我分配。


此外,从我的草稿(3092)中我发现,如果一个类没有用户定义的复制构造函数/赋值运算符,并且没有任何东西阻止移动构造函数/赋值的存在,则会将隐式声明为默认值。如果我没有弄错,这意味着:如果成员是字符串,向量,shared_ptrs等,在这种情况下你通常不会写一个复制构造函数/赋值,你将得到一个移动构造函数/移动赋值免费

答案 1 :(得分:0)

这有什么问题:

struct Object
{
  Object(Object&&o) { ...move stuff...; nullify_object(o); }
  Object & operator = (Object && o) { ...; nullify_object(o); }

  void nullify_object(Object && o);
};

或者在目标上调用nullify_object的替代方法:o.nullify();

我认为添加YANLF没有什么好处。

答案 2 :(得分:0)

移动构造函数/赋值是窃取资源并将被盗对象资源的状态保持在能够在调用析构函数时安全销毁对象的状态。除了移动构造函数/赋值之外,您无法查看或访问从中窃取资源的临时“值”。

举个例子,让我们拿一个字符串。这就是你从临时对象中窃取分配的资源和大小的地方,你可以将它的值设置为你自己的值(如果你的对象是默认构造的,那么它应该为null和0)。