pan*_*ers 5 c++ casting language-lawyer reinterpret-cast type-punning
似乎广泛认为类型惩罚通过reinterpret_cast
某种方式被禁止(正确地说:"未定义的行为",即" 本国际标准没有要求的行为 ",并明确指出实现可以在C++中定义行为).使用以下推理不同意我是不正确的,如果是这样,为什么?
如果类型"指向" 的表达式可以使用a 显式转换为"指向" 的类型,则
T1
可以将类型的glvalue表达式强制转换为"引用T2
"类型.结果引用与源glvalue相同的对象,但具有指定的类型.[注意:也就是说,对于左值,参考演员与使用内置和运算符的转换具有相同的效果(同样适用于). - 结束注释]不创建临时,不进行复制,也不调用构造函数或转换函数.T1
T2
reinterpret_cast
reinterpret_cast<T&>(x)
*reinterpret_cast<T*>(&x)
&
*
reinterpret_cast<T&&>(x)
用脚注:
75)这有时被称为类型双关语.
/ 11隐含地,通过示例,带有/ 6到/ 10的限制,但也许最常见的用法(双关语对象)在[expr.reinterpret.cast]/7中解决:
可以将对象指针显式转换为不同类型的对象指针.当
v
对象指针类型的prvalue 转换为对象指针类型"指向cv T
"时,结果为static_cast<cv T*>(static_cast<cv void*>(v))
.[注意:将"指向T1
"的类型的prvalue转换为"指向"的类型T2
(T1 and T2
对象类型和对齐要求的T2
位置不比那些更严格T1
)并返回其原始类型会产生原始指针值. - 结束说明]
显然,目的不能转换为/从指针或引用void
,如:
static_cast
在指针的情况下应该足够了,[expr.static.cast]/13和[conv.ptr]/2 ; 和void
是初步证据无效.此外,[basic.lval]/8状态:
如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
(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 相同的对象,但具有指定的类型." 这对我来说是一个明确的陈述,即结果reinterpret_cast<T&>(v)
是对类型T
对象的左值引用,访问明显是"通过glvalue of""对象的动态类型".这句话也解决了[basic.life]的各个段落适用于虚假声称的论点,即这种转换的结果是指一种新的类型对象T
,其生命周期尚未开始,恰好存在于相同的内存地址v
.
明确定义此类转换只是为了禁止对结果进行标准定义的使用似乎是荒谬的,特别是根据脚注75,注意到这种[引用]转换"有时被称为类型双关语".
请注意,我的引用是针对C++ 17(N4659)的最终公开草案,但是有问题的语言从N3337(C++ 11)到N4788(C++ 20 WD)几乎没有变化(提示链接)可能会及时参考后面的草稿).事实上,[expr.reinterpret.cast]/11 的脚注在最近的草案中更加明确:
当结果引用与源glvalue相同的对象时,这有时被称为类型双关语.
我相信你的误会在于:
这对我来说是一个明确的陈述,即结果
reinterpret_cast<T&>(v)
是对类型T
对象的左值引用,访问明显是"通过glvalue of""对象的动态类型".
[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);
Run Code Online (Sandbox Code Playgroud)
这里,ui
是一个引用,引用由定义创建的对象my_float
.通过glvalue访问该对象ui
将调用未定义行为(每[basic.lval] /8.1),然而,这是因为动态类型的glvalue的是float
,而类型的glvalue的是std::uint32_t
.
几乎没有reinterpret_cast
类似的有效用途,但是void*
存在除了仅仅是向后和向后的用例(对于后者,static_cast
就足够了,正如你自己注意到的那样).[basic.lval]/8有效地为您提供了它们的完整列表.例如,这将是有效的检查(和甚至复制如果动态对象的类型是平凡-可复制 [basic.types]/9)的对象的值由对象的地址铸造char*
,unsigned char*
或std::byte8
(未signed char*
但是).对于reinterpret_cast
具有签名类型的对象来说,将其作为对应的无符号类型进行访问是有效的,反之亦然.将指针/引用转换为对该联合成员的指针/引用也是有效的,并且如果该成员是该联合的活动成员,则通过结果左值访问该成员...
通常通过这样的强制类型进行类型惩罚的主要原因是,定义行为会禁止一些非常重要的编译器优化.如果允许任何类型的任何对象只是通过任何其他类型的左值进行访问,那么编译器必须假设通过某个左值对对象的任何修改都可能会影响程序中任何对象的值,除非它可以证明不是这样.其结果是,这将基本上是不可能的,例如,在寄存器中保持周围的东西任何时间段非常有用,因为任何事情任何修改会立即失效无论你可能在此刻寄存器.是的,任何好的优化器都会执行别名分析.但是,虽然这些方法确实有效且功能强大,但它们原则上只能涵盖一部分案例.一般来说,反驳或证明混叠基本上是不可能的(相当于解决我认为的停止问题)......