首页 > 解决方案 > reinterpret_cast 类型双关语实际上是未定义的行为吗?

问题描述

似乎普遍认为,在 C++reinterpret_cast中以某种方式禁止类型双关语(正确地:“未定义的行为”,即“本国际标准没有要求的行为”,并明确指出实现可以定义行为)。我使用以下推理不同意是不正确的,如果是,为什么


[expr.reinterpret.cast]/11状态:

如果“pointer to”类型T1的表达式可以使用. 结果引用与源 glvalue 相同的对象,但具有指定的类型。[注意:也就是说,对于左值,引用强制转换与使用内置和运算符的转换具有相同的效果(对于 也是类似的)。— 尾注] 没有创建临时的,没有复制,也没有调用构造函数或转换函数。T2T1T2reinterpret_­castreinterpret_­cast<T&>(x)*reinterpret_­cast<T*>(&x)&*reinterpret_­cast<T&&>(x)

脚注:

75) 这有时被称为双关语类型

/11 通过示例隐含地带有 /6 到 /10 的限制,但也许最常见的用法(双关语对象)在[expr.reinterpret.cast]/7中得到解决:

对象指针可以显式转换为不同类型的对象指针。当v对象指针类型的纯右值转换为对象指针类型“pointer to cv T”时,结果为static_­cast<cv T*>(static_­cast<cv void*>(v))。[注意:将“pointer to”类型的纯右值转换为“pointer to T1”类型T2(其中T1 and T2是对象类型,其中 的对齐要求T2不比 的那些更严格T1)并返回其原始类型会产生原始指针值。——尾注]

显然,目的不能转换为/从指针或引用转换为void,如:

  1. /7 中的示例清楚地表明,static_cast在指针的情况下就足够了,[expr.static.cast]/13[conv.ptr]/2 也是如此;和
  2. [conversions to] 对的引用void初步无效的。

此外,[basic.lval]/8指出:

如果程序尝试通过非下列类型之一的泛左值访问对象的存储值,则行为未定义:

(8.1) 对象的动态类型,

(8.2) 对象的动态类型的 cv 限定版本,

(8.3) 一种类似于对象的动态类型的类型,

(8.4) 与对象的动态类型相对应的有符号或无符号类型,

(8.5) 有符号或无符号类型,对应于对象动态类型的 cv 限定版本,

(8.6) 在其元素或非静态数据成员中包括上述类型之一的聚合或联合类型(递归地包括子聚合或包含联合的元素或非静态数据成员),

(8.7) 一种类型,它是对象的动态类型的(可能是 cv 限定的)基类类型,

(8.8) char、unsigned char 或 std​::​byte 类型。

如果我们返回[expr.reinterpret.cast]/11片刻,我们会看到“结果引用与源 glvalue相同的对象,但具有指定的类型。” 这对我来说是一个明确的声明,即结果是对 typereinterpret_cast<T&>(v)对象的左值引用,显然是“通过”“对象的动态类型”的左值引用。这句话还解决了[basic.life]的各个段落通过虚假声明适用的论点,即此类转换的结果引用类型的新对象,其生命周期尚未开始,恰好位于相同的内存地址。TTv

明确定义这种转换只是为了禁止标准定义的结果使用似乎是荒谬的,特别是鉴于脚注 75 指出这种 [reference] 转换“有时称为类型双关语”。

请注意,我参考的是 C++17 (N4659) 的最终公开可用草案,但所讨论的语言从N3337 (C++11)N4788 (C++20 WD)变化不大(提示链接可能会及时参考以后的草稿)。事实上,[expr.reinterpret.cast]/11的脚注在最近的草稿中更加明确:

当结果引用与源 glvalue 相同的对象时,这有时被称为类型双关语。

标签: c++castinglanguage-lawyerreinterpret-casttype-punning

解决方案


我相信你的误解就在这里:

这对我来说是一个明确的声明,即结果是对 typereinterpret_cast<T&>(v)对象的左值引用,显然是“通过”“对象的动态类型”的左值引用。T

[basic.lval]/8有点误导,因为当动态类型实际上是用于访问对象而不是对象的 glvalue [defns.dynamic.type]的属性时,它谈论的是“对象的”动态类型本身。本质上,glvalue 的动态类型是当前存在于 glvalue 所指位置的对象的类型(实际上,是在该内存块中构造/初始化的对象的类型)[intro.object] /6。例如:

float my_float = 42.0f;
std::uint32_t& ui = reinterpret_cast<std::uint32_t&>(my_float);

在这里,ui是一个引用,它引用由 的定义创建的对象my_float。但是,通过 glvalue 访问此对象ui将调用未定义的行为(根据[basic.lval]/8.1),因为 glvalue 的动态类型float而 glvalue 的类型std::uint32_t

类似这样的有效用法很少reinterpret_cast,但除了转换为void*和返回之外的用例存在(对于后者,static_cast就足够了,正如您自己指出的那样)。[basic.lval]/8有效地为您提供了它们的完整列表。例如,通过将对象的地址转换为 或(不是, 然而)。这将是有效的char*unsigned char*std::byte*signed char*reinterpret_cast一个有符号类型的对象将其作为其对应的无符号类型访问,反之亦然。如果该成员是联合的活动成员,则将联合 的指针/引用转换为对该联合的成员的指针/引用并通过生成的左值访问该成员也是有效的......</p>

像这样通过类型转换的类型双关语通常是未定义的,主要原因是使其定义的行为会禁止一些极其重要的编译器优化。如果您允许通过任何其他类型的左值简单地访问任何类型的任何对象,那么编译器将不得不假设通过某个左值对对象的任何修改都可能影响程序中任何对象的值,除非它可以证明不是这样。因此,基本上不可能,例如,在任何有用的时间段内将内容保存在寄存器中,因为对任何内容的任何修改都会立即使您目前在寄存器中可能拥有的任何内容无效。是的,任何好的优化器都会执行别名分析. 但是,虽然这些方法确实有效且功能强大,但它们在原则上只能涵盖一部分案例。一般来说,反证或证明混叠基本上是不可能的(相当于解决我认为的停机问题)......</p>


推荐阅读