他们经常向 SO 提出有关类型双关操作有效性的问题。例如,我最近贡献了一篇文章:Reinterpret_cast 从 char* 到 uint32_t* CPP 中的未定义行为?.
但是我在回答上述问题时想到了一个问题,我可以用这个片段来说明:
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
struct S {
alignas(std::uint64_t) alignas(std::uint32_t)
std::array<std::byte, sizeof(std::uint64_t)> storage = {some init values};
};
int main()
{
S s;
std::uint64_t* pi64 = std::start_lifetime_as<std::uint64_t>(s.storage.data());
// implicitely creating an uint32_t at the same location, ending lifetime of the uint64_t there
std::uint32_t* pi32 = std::launder(reinterpret_cast<std::uint32_t*>(s.storage.data()));
// is there any guarantee that the value representation is unchanged?
pi64 = std::start_lifetime_as<std::uint64_t>(s.storage);
// is s.storage guaranteed to be unchanged?
}
到目前为止,这只是理论代码,因为据我所知,没有编译器支持
std::start_lifetime_as
。但我认为这有助于说明我的理解问题。
定义
pi64
时,我显式启动一个 std::uint64_t
对象,其值表示在字节级别上是我存储在字节数组中的值。
但随后我隐式创建了一个重叠的
std::uint32_t
,AFAIU,结束了上述 std::uint64_t
对象的生命周期。
底层字节会发生什么?标准规则是什么(我知道
unsigned char
/std::byte
数组有特殊规则)?
但是我隐式地创建了一个重叠的
std::uint32_t
不,你不是。 您编写了
std::launder(reinterpret_cast<std::uint32_t*>(s.storage.data()))
,如果传入 std::uint32_t
的指针处没有 std::launder
对象,则这是未定义的行为(请参阅 std::launder
的Precondition)。
如果你改为这样写,你的例子就会有意义
std::uint32_t* pi32 = std::start_lifetime_as<std::uint32_t>(s.storage.data()));
底层字节会发生什么?
它们保持原样。 [obj.lifetime] p3 解释了对于
std::start_lifetime_as
[新对象]的对象表示是调用
之前存储的内容。 [...] 除了不访问存储之外。start_lifetime_as
std::start_lifetime_as
不访问属于存储的任何字节;它纯粹可以看作是给编译器的指令。
因此,您可以根据需要将任意多个具有相同存储的 std::start_lifetime_as
调用链接在一起,并且对于相同类型,您将始终获得相同的值。