【问题标题】:Can `*this` be `move()`d?`*this` 可以是 `move()`d 吗?
【发布时间】:2015-06-12 18:52:45
【问题描述】:

我想定义一个用于编组数据的类;编组完成后,我想 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()

这是未定义的行为吗?完全没问题吗?或者介于两者之间?有没有更好的方法来实现相同的目标,最好不使用宏或要求调用者明确地movemarshaller

编辑: 使用 G++ 和 Clang++,我发现我不仅可以编译上述用例,而且我实际上可以继续通过编组器对基础数据进行修改,然后使用toDataType 函数重新提取修改后的数据。我还发现marshalled_data中的already-extracted数据继续被marshaller改变,说明marshalled_datamarshaller和调用上下文共享的,所以我怀疑这里有内存泄漏或未定义的行为(来自双重删除)。

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

编辑 3: 这已经足够我打开 another question 来解决我剩下的问题了。

【问题讨论】:

  • 假设两个 marshalled_data 对象不应该引用相同的数据(因为您似乎对它们这样做感到惊讶):如果 marshalled_data 是共享的,那么它听起来像底层的 @987654349 @ 在其复制移动构造函数中有一个错误。例如,它可能根本没有,默认的复制构造函数正在做一个浅拷贝。
  • 移动语义具有破坏性。也就是说,无论您是否已从对象中移动,都会调用对象的析构函数。 std::move 只不过是一个简单的转换,它改变了表达式的值类别。它本身不任何事情。类类型的移动操作的语义完全由类类型定义。
  • 顺便说一句,您的extractData 让我想起了unique_ptr::releasethread::detach 或从其(前)经理释放资源的类似功能。你甚至不需要在这里关心移动语义;只需清楚地指出(通过函数名称及其规范/描述)它将改变/清空资源管理器。
  • 右值引用仍然是像左值引用一样的引用。绑定到它们不会影响绑定的对象。左值和右值引用的区别在于你可以绑定什么样的表达式(值类别)。但是std::move 和一些类似的转换可以改变表达式的值类别。所以我们只有约定。一个右值引用per convention被假定为引用一个我们可以从中窃取资源的对象。该对象的破坏不受“窃取资源”的影响。
  • 复制和交换习语的某些风格间接地做到了这一点:如果他们编写 swap(*this, other) 而没有自定义交换,那么它将使用 C++11 默认实现 std::swap,即三个电话给std::move

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


【解决方案1】:

根据标准,move-from 对象仍然有效,尽管不能保证其状态,因此从*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)();

【讨论】:

  • 那么,哪个上下文控制moved marshaller 对象和moved 底层数据的销毁呢?它们都保证在调用上下文结束时而不是之前被销毁吗?
  • @KyleStrand moved marshaller 对象和moved 基础数据按照正常规则销毁,就好像它们从未被moved 销毁一样。在你 move 离开 marshaller 对象之后,它就不能再用于任何事情了,调用者需要理解这一点。
  • @RaymondChen 调用上下文实际上是对move 之后的基础数据的引用——在正常规则下,它将在范围结束时被销毁,除了 marshaller 对象,它的析构函数也会 尝试销毁底层数据。两次销毁数据成员是未定义的行为(对吗?)。那么编译器真的知道要避免这种情况吗?
  • @KyleStrand 正如我已经指出的,编译器通常会销毁对象。复制移动构造函数的工作是确保销毁不会产生问题。根据您的描述,DataType 的复制移动构造函数似乎存在缺陷。
  • "根据标准,尽管不能保证其状态,但从移动对象仍然有效,因此从*this 移动似乎是完全有效的。"该标准仅定义从标准库对象移出的状态。 我们定义(或不​​)我们的移出对象的状态。一般来说,遵循标准库的规则是一个好主意,因为这种期望在我们大多数人中根深蒂固,但这既不是必需的,也不是保证的。
【解决方案2】:

我会避免从*this 移动,但如果你这样做,至少你应该在函数中添加右值引用限定符:

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()};

【讨论】:

  • 啊。使用 ref-qualifier 看起来是完美的解决方案。
【解决方案3】:

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

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

所以 - 如果你的 extractData 做了它应该做的事情,并且让对象处于允许它稍后被破坏的状态 - 就编译器而言,没有任何未定义或危险的事情发生。当然,代码的 users 可能会出现问题,因为对象被移出并不完全明显(以后可能不会包含任何数据)。通过更改函数名称,这可能会更清楚一点。或者(作为另一个答案建议)&&-qualifying 整个方法。

【讨论】:

  • “事实上,它需要让对象处于“有效但未指定的状态””如何/为什么需要这样做?
  • 该对象将被通常会破坏它的任何进程破坏(delete 或范围结束等)。该操作必须仍然是可能的,这就是“有效状态”部分所指的内容。这是语言唯一要求的——必须保留可破坏性。 “未指定状态”部分只是惯用的协议,即函数可以对对象的内部做任何它想做的事情。大多数情况下,这意味着删除数据并将其存储在其他地方,但它可以做任何事情。该语言对其没有任何特殊限制。
  • 啊,好吧(反正我已经投了赞成票)。我以为您指的是 [lib.types.movedfrom] 中的标准库保证。据我所知,“有效”部分是指类不变量。并非所有这些都必须维护以保持对象可破坏(这是足够的但不是必需的)。
  • 啊,对。我是从语言规范的角度严格考虑的,也许我误用了这个短语。但实际上,实际移动操作的用户指定语义可能对移出对象的状态有进一步的限制。并且保留不变量(就像标准库一样)当然是很好的做法。
  • 我暂时将此标记为已接受的答案,因为它相当清楚并且不认为这是一个 XY 问题。我希望更清楚地了解extractData 对“[做] 应该做的事情”意味着什么,但这可能在我的new question 中得到更好的解决。
【解决方案4】:

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

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

  ...
private:
  DataType data;
};

调用它,如果mMarshaller 变量,则为std::move(m).unwrap()。不需要任何静态成员来完成此操作。

【讨论】:

    【解决方案5】:

    您写道,您希望同时销毁 Marshaller 并从中删除数据。我真的不担心尝试同时做这些事情,只需先将数据移出然后销毁 Marshaller 对象。有很多方法可以不用考虑太多就摆脱 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;
    }
    

    )

    我会避免在 *this 上使用 move ,因为即使它是正确的也会让人们失望。看起来基于堆栈的缓冲区容器可能会给您带来麻烦。

    您似乎担心在编译单元之外再次调用 Marshaller。如果您有密集的并行代码并且在编组器上玩得又快又松,或者您正在随意复制指向编组器的指针,那么我认为您的担心是有道理的。否则,请查看 Marshaller 是如何移动的,并确保您的代码结构具有良好的对象生命周期(尽可能使用对象引用)。您也可以向 marshaller 添加一个成员标志,说明“数据”是否已被移动,如果有人在它离开后尝试访问它,则会引发错误(如果您是并行的,请务必锁定)。我只会将此作为最后的手段或快速修复,因为它看起来不正确,您的共同开发人员会想知道发生了什么。

    如果你有时间,我有一些事要挑:

    • 您的 extractData 方法缺少静态关键字
    • 在 DataType 声明行中混合使用括号和括号

    【讨论】:

    • 对于 unmarshalling,我实际上正在做类似于您所建议的事情(我正在解组的大多数数据类型都有一个采用“unmarshaller”对象的构造函数) .但我特定的“DataType”实际上是 Qt 的QByteArray,它不受我的修改控制。我想我仍然可以定义赋值运算符,但那时我只是在做与toDataType 相同的事情,但不太明确(因为operator= 要求move on它的右操作数)。
    • 关于以意想不到的方式调用 Marshaller 的问题,我实际上只是想知道标准如何处理这样的情况;我不打算写任何特别奇怪的东西,实际上会导致编组器出现意外行为。 (我的编组器的寿命很短。)
    • 感谢您注意到混合括号/括号。 static 错字已经被其他几个人发现了。 (这就是我为了在此处发布而尝试使我的代码“通用”的结果......)
    • 另一种设计方法是让 Marshaller 引用 DataType 的实例并简单地“填充”。在我看来,这比使用右值引用和移动语义更简单
    猜你喜欢
    • 2014-10-09
    • 1970-01-01
    • 2012-10-24
    • 2011-10-13
    • 1970-01-01
    • 2018-04-06
    • 2012-12-05
    • 2016-07-21
    • 1970-01-01
    相关资源
    最近更新 更多