首页 > 解决方案 > 什么时候可以安全地重用来自可简单破坏的对象的内存而不进行清洗

问题描述

关于以下代码:

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

我上面的例子是明确定义的行为吗?我需要做什么才能使其工作?工会会做这项工作吗?

标签: c++unionc++17placement-newtype-punning

解决方案


在您的代码中,您确实需要std::launderreinterpret_cast您想做的事情。这是与重用内存不同的问题。根据标准([expr.reinterpret].cast]7),你的表达

reinterpret_cast<One*>(&storage)

相当于:

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

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

如果原始指针值指向对象a,并且有一个类型为bT的对象(忽略 cv 限定)与a指针可互转换(6.9.2) ,则结果是指向b的指针。否则,指针值不会因转换而改变。

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

如果按照评论中的建议,您使用了 union 而不是aligned_storage,

union {
    One one;
    Two two;
};

您将回避指针互转换问题,因此std::launder由于非指针互转换而不需要。但是,仍然存在内存重用的问题。在这种特殊情况下,std::launder由于重用而不需要,因为您的OneTwo类不包含任何const-qualified 或引用类型 ([basic.life]/8) 的非静态数据成员。

最后,还有一个问题,为什么 libstdc++ 的实现std::optional不使用std::launder,即使可能包含包含-qualified 或引用类型std::optional的非静态数据成员的类。const正如评论中所指出的,libstdc++ 是实现的一部分,std::launder当实现者知道 GCC 仍然可以在没有它的情况下正确编译代码时,可能会简单地忽略它。导致引入的讨论std::launder(参见CWG 1776和链接的线程,N4303P0137)似乎表明,在比我更了解标准的人看来,std::launder确实需要为了使基于联合的实现std::optional在存在const-qualified 或 reference 类型的成员时定义良好。但是,我不确定标准文本是否足够清晰以使这一点显而易见,并且可能值得讨论如何澄清它。


推荐阅读