Can `*this` be `move()`d?

Can `*this` be `move()`d?

本文关键字:be this Can move      更新时间:2023-10-16

我想定义一个用于编组数据的类;当封送完成时,我想从其中move封送的数据,这可能会使封送对象无效。

我相信使用下面的static函数extractData是可能的:

class Marshaller
{
  public:
    static DataType extractData(Marshaller&& marshaller)
    {
      return std::move(marshaller.data);
    }
  private:
    DataType data;
}

不过,这有点不方便打电话:

Marshaller marshaller;
// ... do some marshalling...
DataType marshalled_data{Marshaller::extractData(std::move(marshaller))};

那么我可以用一个成员函数来包装它吗?

DataType Marshaller::toDataType()
{
  return Marshaller::extractData(std::move(*this));
}

当然,这将被称为使用:

DataType marshalled_data{marshaller.toDataType()};

对我来说,它看起来好多了。但是std::move(*this)看起来非常可疑。在调用toDataType()的上下文中,marshaller不能再次使用,但我认为编译器不知道:函数的主体可能在调用方的编译单元之外,因此没有任何迹象表明marshaller已经应用了move()

这是未定义的行为吗?它完全好吗?或者介于两者之间?是否有更好的方法来实现相同的目标,最好不使用宏或要求调用方显式move marshaller

EDIT:使用G++和Clang++,我发现我不仅可以编译上述用例,而且实际上我可以通过marshaller继续对底层数据进行修改,然后使用toDataType函数重新提取修改后的数据。我还发现marshalled_data中已经提取的数据继续被marshaller更改,这表明marshalled_datamarshaller和调用上下文之间共享,因此我怀疑这里存在内存泄漏或未定义的行为(来自双重删除)。

EDIT 2:如果我将print语句放入DataType的析构函数中,当调用方离开作用域时,它会出现两次。如果我在DataType中包含一个包含数组的数据成员,并具有相应的new[]delete[],则会得到glibc"双重自由或损坏"错误。所以我不确定这个怎么会安全,尽管有几个答案说它在技术上是允许的。一个完整的答案应该解释在一个非平凡的DataType类中正确使用该技术需要什么。

第3版:这已经够多了,我提出了另一个问题来解决我剩下的问题。

根据标准,从对象移动仍然有效,尽管它的状态没有得到保证,所以从*this移动似乎是完全有效的。这是否会让代码的用户感到困惑完全是另一个问题。

所有这些听起来像是你真正的意图是将马歇尔的破坏与数据的提取联系起来。你有没有考虑过在一个表达中完成所有的编组,让一个临时的人为你处理事情?

class Marshaller
{
  public:
    Marshaller& operator()(input_data data) { marshall(data); return *this; }
    DataType operator()() { return std::move(data_); }
  private:
    DataType data_;
}
DataType my_result = Marshaller()(data1)(data2)(data3)();

我会避免从*this移动,但如果您这样做,至少您应该向函数添加右值ref限定符:

DataType Marshaller::toDataType() &&
{
    return Marshaller::extractData(std::move(*this));
}

这样,用户将不得不这样称呼它:

// explicit move, so the user is aware that the marshaller is no longer usable
Marshaller marshaller;
DataType marshalled_data{std::move(marshaller).toDataType()};
// or it can be called for a temporary marshaller returned from some function
Marshaller getMarshaller() {...}
DataType marshalled_data{getMarshaller().toDataType()};

调用move(*this)并没有本质上的不安全。move本质上只是对被调用函数的一个提示,它可能会窃取对象的内部。在类型系统中,这种承诺是通过&&引用来表达的。

这与破坏没有任何关系。move不执行任何类型的破坏——如前所述,它只是使我们能够调用使用&&参数的函数。接收移动对象的功能(在这种情况下为extractData)也不进行任何破坏。事实上,它需要使对象处于"有效但未指定的状态"。从本质上讲,这意味着必须能够以正常方式销毁对象(通过delete或超出范围,具体取决于它是如何创建的)。

因此,只要extractData做了它应该做的事情,并使对象处于允许稍后对其进行销毁的状态,那么编译器就不会发生任何未定义或危险的事情。当然,用户混淆代码可能会有问题,因为对象是否从中移动并不完全明显(以后可能不会包含任何数据)。也许可以通过更改函数名称来使这一点更加清晰。或者(正如另一个答案所建议的)通过&&——对整个方法进行限定。

我认为您不应该从*this移动,而应该从它的data字段移动。由于这显然会使Marshaller对象处于有效但不可用的状态,因此执行此操作的成员函数本身应该在其隐式*this参数上具有右值引用限定符。

class Marshaller
{
public:
  ...
  DataType Marshaller::unwrap() &&   { return std::move(data); }
  ...
private:
  DataType data;
};

如果mMarshaller变量,则将其调用为std::move(m).unwrap()。不需要任何静态成员来实现这一点。

您写道,您希望同时销毁Marshaller并从中删除数据。我真的不担心同时尝试做这些事情,只需先将数据移出,然后销毁Marshalller对象。有很多方法可以不用多想就摆脱Marshaller,也许一个智能指针对你来说有意义?

重构的一个选项是为DataType提供一个构造函数,该构造函数使用Marshaller并将数据移出("friend"关键字将允许您这样做,因为DataType将能够访问该私有的"data"变量)。

    //add this line to the Marshaller
    friend class DataType;
struct DataType
{
    DataType(Marshaller& marshaller) {
             buffer = marshaller.data.buffer;
        }
    private: 
        Type_of_buffer buffer;//buffer still needs to know how to have data moved into it
}

你也可以给它一个做同样事情的赋值操作符(我认为这只会起作用:

DataType& operator=(Marshaller&& marshaller) {
     this.buffer = std::move(marshaller.data.buffer);
     return *this;
}

)

我会避免在这个问题上使用move,因为即使它是正确的,它也会让人们反感。基于堆栈的缓冲容器似乎也会给您带来麻烦。

您似乎担心Marshaller在编译单元外再次被调用。如果你有密集的并行代码,并且对marshaller反复无常,或者你随意地将指针复制到marshaller,那么我认为你的担忧是合理的。否则,请查看Marshaller是如何移动的,并确保您已经为良好的对象生存期构建了代码(尽可能使用对象引用)。您也可以向marshaller添加一个成员标志,说明"数据"是否已被移动,如果有人试图在数据离开后访问它,则抛出错误(如果您是并行的,请确保锁定)。我只会把这作为最后的手段或快速解决方案,因为这似乎不正确,你的合作开发人员会想知道发生了什么。

如果你有时间的话,我有一些挑剔的地方:

  • extractData方法缺少static关键字
  • 在DataType声明行中混合使用括号和圆括号