自动生成的移动构造函数导致非法行为

时间:2015-04-13 20:24:10

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

我问a question about move constructors我尚未接受答案,因为即使我开始抓住其他人,我对这个问题的某些方面感到更加困惑。特别是,我发现了一个令人惊讶的案例,其中g ++和clang ++都生成了错误的move-constructors。

问题摘要

  • g ++和clang ++显然违反了在明确定义析构函数时不生成move-constructors的规则;为什么?这是一个错误,还是我误解了发生了什么?
  • 为了正确,这些(可能是非法的)移动构造函数应该使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[]是一个数组(!!)时,我得到了相同的行为,我甚至能够编译需要一个有效的移动构造函数(!!!!!)的代码。 (见下面的例子。)这里发生了什么?在这里自动生成移动构造函数是否合法,如果是,为什么?

第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,似乎也没有任何理由使{{1}无效在自动生成的版本中,除了我想,因为这会导致轻微的性能损失。

因此,如果编译器自动生成此方法 - 再次,它不应该,特别是因为我们可以使用{{1从标准库代码轻松获取此确切行为 - 为什么会自动生成错误

第3部分:在Qt中避免此行为(尤其是Qt 5.4中的rhs

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

我甚至懒得问,因为Qt看起来非常稳固,我还没有看到任何双重删除错误尚未,但鉴于我被这些不正确的编译器取消了警惕 - 生成移动构造函数,我担心在一个实现良好的库中最终会出现错误的移动构造函数。

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

1 个答案:

答案 0 :(得分:7)

问题是你混淆了is_move_constructible和“有一个移动构造函数”。 is_move_constructible<T>不测试T是否有移动构造函数。它测试T是否可以从T类型的右值构造。 const T&可以绑定到T rvalue。

您所看到的是自动生成的副本构造函数T(const T&)正在执行其工作 - 并且失败了。

  

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

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

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

  

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