【问题标题】:Is reinterpret_cast type punning actually undefined behavior?reinterpret_cast 类型双关语实际上是未定义的行为吗?
【发布时间】:2019-05-28 11:17:39
【问题描述】:

似乎普遍认为通过reinterpret_cast 进行类型双关语在某种程度上是被禁止的(正确地:“未定义的行为”,即“behavior for which this International Standard imposes no requirements”,并明确指出实现可能 定义行为)在 C++ 中。我是否不正确地使用以下理由不同意,如果是,为什么


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

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

带脚注:

75) 这有时被称为双关语

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

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

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

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

此外,[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,但具有指定的类型。 "这在我看来是一个明确的声明,即reinterpret_cast<T&>(v) 的结果是对T 类型的对象的左值引用,显然是“通过”动态类型的“左值”访问该对象对象”。这句话还解决了[basic.life] 的各个段落通过虚假声明适用的论点,即此类转换的结果引用了T 类型的新对象,其生命周期尚未开始,刚刚发生 驻留在与v 相同的内存地址。

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

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

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

【问题讨论】:

  • 我没有注意到这种信念甚至被狭隘地持有。
  • 这对我来说是一个明确的声明,即 reinterpret_cast(v) 的结果是对 T 类型对象的左值引用,显然是“通过左值访问” of" "对象的动态类型"。不,你错了。我不明白你为什么这么想。如果你访问结果,你通过T类型访问它,而不是v的动态类型。
  • glvalue 没有动态类型。 glvalue 是一个值,而值只有一个类型。唯一具有动态类型的实体是对象。值不是对象。
  • 不存在T 类型的对象(r​​einterpret_cast 不创建对象)
  • @MichaelKenzel 不; 动态类型 是表达式具有的属性(它不是对象的属性)。一个对象只有它的类型。在表达式指定基类子对象的情况下,表达式可能具有与其静态类型不同的动态类型

标签: c++ casting language-lawyer reinterpret-cast type-punning


【解决方案1】:

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

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

[basic.lval]/8 有点误导,因为当动态类型实际上是用于访问对象而不是对象本身的glvalue [defns.dynamic.type] 的属性时,它谈论的是“对象的”动态类型。本质上,glvalue 的动态类型是 is currently living 在 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 的 动态类型floattype glvalue 的值是std::uint32_t

像这样的reinterpret_cast 的有效用途很少,但除了转换为void* 并返回之外的用例存在(对于后者,static_cast 就足够了,正如您自己指出的那样)。 [basic.lval]/8 有效地为您提供了它们的完整列表。例如,通过将对象的地址转换为 @987654343 来检查(如果对象的动态类型是 trivially-copyable [basic.types]/9,甚至复制)对象的值是有效的@、unsigned char*std::byte8(但不是 signed char*)。 reinterpret_cast 一个有符号类型的对象将其作为其对应的无符号类型访问它是有效的,反之亦然。将指向联合的指针/引用转换为指向该联合成员的指针/引用并通过生成的左值 if 访问该成员也是有效的,该成员是联合的活动成员…

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

【讨论】:

  • A reinterpret_cast not 返回一个对象。它是一个表达式,其计算结果为 points引用 一个对象的值。该标准没有说明被更改对象的类型。它只是声明返回一个具有不同类型但 引用 相同对象的泛左值。我们在这里讨论的不是符号转换,而是将对象的值解释为另一种类型,这与转换根本上不同。参考转换可用于上述 [basic.lval]/8 中列出的某些情况。
  • 再一次,有些事情只能通过reinterpret_cast来实现。例如,如果没有reinterpret_cast,您将如何将指针转换为整数并返回?您如何将函数指针转换为不同的函数指针类型并返回?仅仅因为在某些情况下reinterpret_cast 实际上只是一种速记或只是获得相同结果的几种方法之一并不意味着它根本没有理由存在。仅仅因为不是功能 X 的所有可能使用也是对该功能的有效使用,并不意味着没有对功能 X 的有效使用......
  • "类型 T 的对象的生命周期开始于: (1.1) 获得类型 T 的正确对齐和大小的存储" 所以那里有一个对象。
  • @curiousguy 抱歉,我不知道你在说什么。在任何给定时刻,任何一块内存中都只能有一个完整的对象。一旦在某个存储中创建了另一个对象,则当前可能驻留在其中的任何对象的生命周期都会结束[basic.life]/1.4。对象概念的定义特征之一是对象具有标识。在 C++ 对象模型中,这意味着没有两个完整的对象可以具有相同的地址[intro.object]/9
  • 我不明白关于 C++ 对象模型的潜在优点或缺乏的哲学讨论与这里的问题或我的回答有何关系。上面的引用涉及对象的生命周期。在谈论对象的生命周期之前,您首先需要谈论一个对象。 [basic.memobj]/1 明确指定了哪些语言结构会产生对象。仅仅因为 C++ 标准并非绝对完美,并不意味着对任何事情都无话可说,任何解释都同样有效……
【解决方案2】:

[basic.lval]/8 表示行为何时肯定是未定义的,但这并不一定意味着如果您从 [basic.lval]/8 中的列表中执行某些操作,则行为将被定义。

[basic.lval]/8 自 C++98 以来没有太大变化,并且它的措辞不准确,例如使用了未定义的术语“对象的动态类型”。 (C++ 定义了表达式的动态类型)。

如果您执行 [basic.lval]/8 允许的操作,行为定义取决于标准的其他部分。即使可以同意签名/未签名重新解释的结果可以来自 [basic.types] 中的措辞,我无法想象如何预测访问包含引用或虚拟方法的对象的结果通过char glvalue。

C++17 的新指针和 glvalue 转换规则使 [basic.lval]/8 更加无用,因为现在无法正式实现 [basic.lval]/8 旨在保证的目标(例如,通过char glvalue 读取对象中的字节)。正如您所指出的,根据 [expr.reinterpret.cast]/7,在 reinterpret_cast 对 T 的引用之后,生成的 glvalue 仍然引用 reinterpret_cast 的参数所引用的对象。

根据[conv.lval]/(3.4),左值到右值转换的结果是包含在转换后的左值引用的对象中的值。例如,这些规则意味着应用于reinterpret_cast&lt;char&amp;&gt;(i)(其中iint 变量)的左值到右值转换的结果是存储在iint 对象中的值。 prvalue 的类型是char ([conv.lval]/1),如果i 的值不能由char 表示,则根据[expr]/1,行为是未定义的。如果 char 不能表示对象的值,即使 [basic.lval]/(8.8) “允许”此访问,尝试通过 char 泛左值读取 int 对象将导致 UB。这证明了第一段所说的。

【讨论】:

  • 基础级别的 C++ 规范看起来像火车残骸
  • @curiousguy 修复了答案。实际上,如果 int 对象不能用 char 类型表示,则您不会得到它的值,这将是 UB。
【解决方案3】:

如果中间类型具有相同或不太严格的对齐要求,则可以将使用 reinterpret_cast 构建的引用(我包括转换指针然后解除引用)往返到原始类型。

由于严格的别名规则,大多数其他用途都是未定义的行为。 (不需要语言引用,因为问题已经引用了它)

表达式最终类型与对象的动态类型不匹配的值得注意的法律案例包括通过窄字符类型产生别名,以及结构的常见初始序列规则。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-24
    • 1970-01-01
    • 2016-09-04
    • 2018-07-04
    • 1970-01-01
    • 2015-05-17
    相关资源
    最近更新 更多