什么时候可以安全地重用普通可破坏对象的内存而无需清洗

时间:2019-02-07 20:31:03

标签: c++ union c++17 placement-new type-punning

关于以下代码:

class One { 
public:
  double number{};
};

class Two {
public:
  int integer{};
}

class Mixture {
public:
  double& foo() {
    new (&storage) One{1.0};
    return reinterpret_cast<One*>(&storage)->number;
  }

  int& bar() {
    new (&storage) Two{2};
    return reinterpret_cast<Two*>(&storage)->integer;
  }

  std::aligned_storage_t<8> storage;
};

int main() {
  auto mixture = Mixture{};
  cout << mixture.foo() << endl;
  cout << mixture.bar() << endl;
}

我没有为这些类型调用析构函数,因为它们很容易被破坏。我对标准的理解是,为了安全起见,我们需要先清洗指向存储的指针,然后再将其传递给reinterpret_cast。但是,libstdc ++中的std :: optional的实现似乎并不使用std::launder(),而只是将对象构造到联合存储中。 https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/optional

我的示例是否已明确定义了行为?我需要做些什么才能使其正常工作?工会会做这项工作吗?

1 个答案:

答案 0 :(得分:1)

在您的代码中,您确实需要std::launder才能使您的reinterpret_cast做您想做的事情。这与重新使用内存是一个单独的问题。根据标准[[expr.reinterpret] .cast] 7,您的表情

reinterpret_cast<One*>(&storage)

等效于:

static_cast<One*>(static_cast<void*>(&storage))

但是,根据[expr.static.cast] / 13,外部static_cast无法成功生成指向新创建的One对象的指针,

  

如果原始指针值指向对象 a ,并且存在类型为T(忽略cv限定)的对象 b ,则该指针为指针-可相互转换(6.9.2)   使用 a 时,结果是指向 b 的指针。否则,转换后指针值将保持不变。

也就是说,结果指针仍然指向storage对象,而不是嵌套在其中的One对象,并且将其用作指向One对象的指针会违反严格的别名规则。您必须使用std::launder强制结果指针指向One对象。或者,如注释中所指出的,您可以直接使用由new放置直接返回的指针,而不是使用从reinterpret_cast获得的指针。

如果按照注释中的建议使用联合而不是aligned_storage

union {
    One one;
    Two two;
};

您将回避指针互换性问题,因此由于非指针互换性,将不需要std::launder。但是,仍然存在重复使用内存的问题。在这种特定情况下,由于您的std::launderOne类不包含任何Two限定的非静态数据成员,因此由于重复使用而不需要const或参考类型([basic.life] / 8)。

最后,存在一个问题,为什么即使std::optional可能包含包含{{1}的非静态数据成员的类,libstdc ++的std::launder的实现也不使用std::optional }或参考类型。正如评论中指出的那样,libstdc ++是实现的一部分,并且在实现者知道GCC仍将在没有它们的情况下正确编译代码时,可能会简单地忽略const。理解以下内容的人认为,导致引入std::launder(参见CWG 1776和链接的线程N4303P0137)的讨论似乎表明了这一点。该标准比我做的要好得多,实际上确实需要使用std::launder,以便在存在std::launder合格或引用类型的成员的情况下,使基于std::optional的基于联合的实现得到良好定义。但是,我不确定标准文本是否足够清晰以至于使其变得明显,因此可能值得讨论如何对其进行澄清。