reinterpret_cast类型是否实际上是未定义的行为?

pan*_*ers 5 c++ casting language-lawyer reinterpret-cast type-punning

似乎广泛认为类型惩罚通过reinterpret_cast某种方式被禁止(正确地说:"未定义的行为",即" 本国际标准没有要求的行为 ",并明确指出实现可以在C++中定义行为).使用以下推理不同意我是不正确的,如果是这样,为什么


[expr.reinterpret.cast]/11州:

如果类型"指向" 的表达式可以使用a 显式转换为"指向" 的类型,则T1可以将类型的glvalue表达式强制转换为"引用T2"类型.结果引用与源glvalue相同的对象,但具有指定的类型.[注意:也就是说,对于左值,参考演员与使用内置和运算符的转换具有相同的效果(同样适用于). - 结束注释]不创建临时,不进行复制,也不调用构造函数或转换函数.T1T2reinterpret_­castreinterpret_­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,如:

  1. / 7中的例子清楚地表明,static_cast在指针的情况下应该足够了,[expr.static.cast]/13[conv.ptr]/2 ; 和
  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相同的对象时,这有时被称为类型双关语.

Mic*_*zel 7

我相信你的误会在于:

这对我来说是一个明确的陈述,即结果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具有签名类型的对象来说,将其作为对应的无符号类型进行访问是有效的,反之亦然.将指针/引用转换为对该联合成员的指针/引用也是有效的,并且如果该成员是该联合的活动成员,则通过结果左值访问该成员...

通常通过这样的强制类型进行类型惩罚的主要原因是,定义行为会禁止一些非常重要的编译器优化.如果允许任何类型的任何对象只是通过任何其他类型的左值进行访问,那么编译器必须假设通过某个左值对对象的任何修改都可能会影响程序中任何对象的值,除非它可以证明不是这样.其结果是,这将基本上是不可能的,例如,在寄存器中保持周围的东西任何时间段非常有用,因为任何事情任何修改会立即失效无论你可能在此刻寄存器.是的,任何好的优化器都会执行别名分析.但是,虽然这些方法确实有效且功能强大,但它们原则上只能涵盖一部分案例.一般来说,反驳或证明混叠基本上是不可能的(相当于解决我认为的停止问题)......

  • 我不明白对C++对象模型的潜在优点或缺乏的哲学讨论如何与这里的问题或我的答案相关.上面的引用涉及对象的生命周期.在谈论对象的生命周期之前,首先需要一个对象来讨论.[\ [basic.memobj \]/1](http://eel.is/c++draft/basic.memobj#intro.object-1)清楚地指明了什么语言结构会产生对象.仅仅因为C++标准并非完全无瑕疵并不意味着什么都不能说什么,任何解释都同样有效...... (4认同)
  • 同样,有些事情只能通过`reinterpret_cast`来实现.例如,如何在不使用`reinterpret_cast`的情况下将指针转换为整数并返回?你如何将函数指针转换为不同的函数指针类型并返回?仅仅因为有些情况下`reinterpret_cast`实际上只是一种速记,或只是获得相同结果的几种方法之一并不意味着它根本没有任何理由存在.仅仅因为并非每种可能的功能X的使用也是对该功能的有效使用并不意味着没有功能X的有效用途...... (3认同)
  • `reinterpret_cast`确实*不*返回一个对象.它是一个表达式,其值为*points*或*指向对象的*.该标准没有说明要更改的对象的类型.它只是声明返回一个glvalue,它具有不同的类型但*引用*到同一个对象.我们不是在讨论这里的符号转换,我们讨论的是将对象的值解释为另一种类型,它与转换*基本上*不同.如上所述,可以在[basic.lval]/8中列出的一些情况中使用参考转换. (2认同)
  • @curiousguy抱歉,我不知道你在谈论什么.在任何给定时刻,任何一块内存中只能有一个完整的对象存活.一旦在某个存储区中创建了另一个对象,那么任何对象的生命周期当前都可以驻留在那里[\ [[.basic.life \] /1.4](http://eel.is/c++draft/basic .生命#1.4).对象概念的一个定义特征是对象具有身份.在C++对象模型中,这意味着没有两个完整对象可以具有相同的地址[\ [[intro.object \]/9](http://eel.is/c++draft/intro.object#9). (2认同)