关于以下代码:
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。
我的示例是否已明确定义了行为?我需要做些什么才能使其正常工作?工会会做这项工作吗?
答案 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::launder
和One
类不包含任何Two
限定的非静态数据成员,因此由于重复使用而不需要const
或参考类型([basic.life] / 8)。
最后,存在一个问题,为什么即使std::optional
可能包含包含{{1}的非静态数据成员的类,libstdc ++的std::launder
的实现也不使用std::optional
}或参考类型。正如评论中指出的那样,libstdc ++是实现的一部分,并且在实现者知道GCC仍将在没有它们的情况下正确编译代码时,可能会简单地忽略const
。理解以下内容的人认为,导致引入std::launder
(参见CWG 1776和链接的线程N4303,P0137)的讨论似乎表明了这一点。该标准比我做的要好得多,实际上确实需要使用std::launder
,以便在存在std::launder
合格或引用类型的成员的情况下,使基于std::optional
的基于联合的实现得到良好定义。但是,我不确定标准文本是否足够清晰以至于使其变得明显,因此可能值得讨论如何对其进行澄清。