【问题标题】:Autogenerated move constructors causing illegal behavior自动生成的移动构造函数导致非法行为
【发布时间】:2015-04-13 20:24:09
【问题描述】:

我问过a question about move constructors,我还没有接受答案,因为我对这个问题的某些方面感到更加困惑,即使我开始掌握其他方面。特别是,我发现了一个令人惊讶的案例,其中 g++ 和 clang++ 都生成了不正确的移动构造函数。

问题总结

  • g++ 和 clang++ 显然违反了明确定义析构函数时不生成移动构造函数的规则;为什么?这是一个错误,还是我误解了发生了什么?
  • 为了正确起见,这些(可能是非法的)移动构造函数应该使 RHS 指针成员无效,但它们不会。为什么不呢?
  • 看来,避免不良行为的唯一方法是为在其析构函数中使用delete 的每个类显式定义一个正确 移动构造函数。 Qt 库(5.4 版)会这样做吗?

第 1 部分:非法自动生成的构造函数?

考虑以下代码:

class NoMove
{
  public:
    ~NoMove() {}
};
int main()
{
  std::cout << "NoMove move-constructible? " <<
    std::is_move_constructible<NoMove>::value << std::endl;
}

使用g++ 4.9.2 和clang++ 3.5.1 编译,此代码打印:

NoMove move-constructible? 1

...但是由于NoMove 有一个明确定义的析构函数,我希望neither a move constructor nor a copy constructor should be auto-generated。请注意,意外的构造函数生成并不是因为析构函数是微不足道的;当析构函数 delete[]s 是一个数组 (!!) 时,我会得到相同的行为,我什至能够编译 需要 有效移动构造函数 (!!!!!) 的代码。 (见下面的例子。)这里发生了什么?在这里自动生成移动构造函数是否合法,如果是,为什么?

第 2 部分:(可能是非法的)自动生成的构造函数导致未定义的行为?

似乎在涉及delete 时提供安全移动构造函数is fairly simple,但我只是想确保我理解:当一个类包含指针成员并且拥有基础数据时,是否存在任何种情况,在将目标指针设置为旧值后,移动构造函数使 RHS 指针无效?

考虑以下示例,它与上面的NoMove 示例类似,并且基于我的original question

class DataType
{
  public:
    DataType()
    {
      val = new int[35];
    }
    ~DataType()
    {
      delete[] val;
    }
  private:
    int* val;
};

class Marshaller
{
  public:
    Marshaller()=default;
    DataType toDataType() &&
    {
      return std::move(data);
    }
  private:
    DataType data;
};

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

这编译得很好——表明,是的,DataType 有一个自动生成的移动构造函数。当然,在运行时,它会导致双重删除错误。

现在,这没关系,如果自动生成的移动构造函数使 RHS 指针无效。所以,如果可以在这里自动生成移动构造函数,为什么不这样做安全?使这项工作的移动构造函数很简单:

DataType(DataType&& rhs) :
  val{rhs.val}
{
  rhs.val = nullptr;
}

(对吗?我错过了什么吗?应该是val{std::move(rhs.val)}?)

这似乎是一个非常安全的自动生成功能;编译器知道 rhs 是一个 r 值,因为函数原型是这样说的,因此修改它是完全可以接受的。因此,即使DataType 的析构函数没有 delete[] val,似乎也没有任何理由 使rhs 在自动-生成的版本,但我认为这会导致微不足道的性能损失。

因此,如果编译器正在自动生成这个方法——同样,它不应该,特别是因为我们可以使用unique_ptr 从标准库代码中轻松获得这种精确行为-- 为什么它会自动生成它不正确

第 3 部分:在 Qt 中避免这种行为(尤其是 Qt 5.4 中的 QByteArray

最后,一个(希望如此)简单的问题:Qt 5.4 的堆分配类,如 QByteArray(这是我在原始问题中实际使用的 DataType)正确实现了移动构造函数,使任何移出的拥有指针无效?

我什至懒得问,因为 Qt 看起来很可靠,而且我还没有看到任何双重删除错误,但考虑到这些不正确的编译器让我措手不及——生成的移动构造函数,我担心在其他实现良好的库中很容易得到不正确的移动构造函数。

与此相关,在C++11 之前编写的没有显式移动构造函数的 Qt 库呢?如果我不小心强制了一个在这种情况下行为错误的自动生成的移动构造函数,有没有人知道使用符合 C++11 的编译器编译 Qt 3 是否会在这样的用例中导致未定义的破坏行为?

【问题讨论】:

  • 我投票结束这个问题,因为老兄。每个问题一个问题。
  • @Barry 这让它……跑题了?你真的读过全文吗?这些问题非常相关,联系在一起,甚至是累积的。简化后,它们可能是:“1)为什么标准编译器在这种情况下会生成一个 move-ctor?2)为什么生成的 ctor 不正确?3)Qt 是否避免了这个错误?”
  • 我确实阅读了整本书。此问题包含三个单独的问题。
  • 我认为,如果你把它分成更小的问题,你会得到更好的结果,因为你不太可能一口气得到一本必要的小说来回答所有这些问题。
  • @barry 我不同意,但我知道你会如何得出这个结论。我已经重写了问题的某些部分,以尝试以更统一的方式呈现它。

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


【解决方案1】:

问题是你混淆了is_move_constructible 并且“有一个移动构造函数”。 is_move_constructible&lt;T&gt; 不测试 T 是否有移动构造函数。它测试是否可以从T 类型的右值构造T。而const T&amp; 可以绑定到T 右值。

您所看到的是自动生成的 copy 构造函数 T(const T&amp;) 正在完成它的工作 - 并且惨遭失败。

我希望移动构造函数和复制构造函数都不应该自动生成。

您的链接谈到了移动构造函数。它没有谈论复制构造函数,如果你不声明它总是隐式声明的。

现在,如果你声明了一个移动操作,隐式声明的复制构造函数将被定义为删除,但你没有这样做,所以它被定义为默认值并执行成员复制。 [class.copy]/p7:

如果类定义没有显式声明一个副本 构造函数,一个是隐式声明的。如果类定义 声明一个移动构造函数或移动赋值运算符, 隐式声明的复制构造函数被定义为已删除;否则, 它被定义为默认值(8.4)。后一种情况被弃用,如果 类具有用户声明的复制赋值运算符或用户声明的 析构函数。

【讨论】:

  • ....啊。我读过这种自动生成的构造函数已被弃用,但我记错了这一事实,因为“自动生成复制构造函数比自动生成移动构造函数更具限制性”。我还以为我通过显式删除复制构造函数来测试这种可能性,但我想我忘了这样做。
  • 看起来 Clang 使用“-Wdeprecated”对此发出警告,但 GCC 4.9.2 没有。这是一个错误吗?
  • @KyleStrand 不是从标准的角度来看。该标准不需要使用已弃用功能的诊断消息。
  • 如果标准要求它,为什么首先需要-Wdeprecated-Wdeprecated 的意义不是针对语言弃用发出警告吗?
猜你喜欢
  • 1970-01-01
  • 2012-01-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-09
  • 2011-08-21
  • 1970-01-01
  • 2012-03-08
相关资源
最近更新 更多