【问题标题】:Unions, aliasing and type-punning in practice: what works and what does not?实践中的联合、别名和类型双关:什么有效,什么无效?
【发布时间】:2019-02-19 08:54:02
【问题描述】:

我无法理解使用带有 GCC 的联合可以做什么和不可以做什么。我阅读了有关它的问题(特别是 herehere),但它们关注的是 C++ 标准,我觉得 C++ 标准和实践(常用的编译器)之间存在不匹配。

特别是,我最近在阅读有关编译标志 -fstrict-aliasing 的信息时,在 GCC online doc 中发现了令人困惑的信息。它说:

-fstrict-aliasing

允许编译器采用适用于正在编译的语言的最严格的别名规则。对于 C(和 C++),这会根据表达式的类型激活优化。特别是,假设一种类型的对象永远不会与不同类型的对象驻留在相同的地址,除非类型几乎相同。 例如,unsigned int 可以别名为 int,但不能为 void*double。字符类型可以别名任何其他类型。 特别注意这样的代码:

union a_union {
  int i;
  double d;
};

int f() {
  union a_union t;
  t.d = 3.0;
  return t.i;
}

从不同的工会成员阅读而不是最近写信的人(称为“类型双关语”)的做法很常见。 即使使用 -fstrict-aliasing,也允许使用类型双关语,前提是通过联合类型访问内存。因此,上面的代码按预期工作。

这是我认为我从这个例子和我的疑惑中理解的:

1)别名仅适用于相似类型或字符之间

1) 的后果: 别名——顾名思义——是当你有一个值和两个成员来访问它时(即相同的字节);

疑问:当它们具有相同的字节大小时,两种类型是否相似?如果不是,类似的类型有哪些?

1) 的后果 对于不相似的类型(无论这意味着什么),别名不起作用;

2) 类型的双关语是指我们读到的成员与我们写信的成员不同;这很常见,只要通过联合类型访问内存,它就可以按预期工作;

疑问: 是在类型相似的类型双关语中使用别名吗?

我很困惑,因为它说 unsigned int 和 double 不相似,所以别名不起作用;然后在示例中它是 int 和 double 之间的别名,它清楚地表明它按预期工作,但称之为类型双关语: 不是因为类型相似或不相似,而是因为它是从它没有写入的成员中读取的。但是从它没有写的成员那里读取是我理解别名的用途(正如这个词所暗示的那样)。我迷路了。

问题: 有人可以澄清别名和类型双关语之间的区别以及这两种技术的哪些用途在 GCC 中按预期工作?编译器标志有什么作用?

【问题讨论】:

  • “我觉得规范和实践不匹配” 直到你升级你的编译器,一切都会造成严重破坏! (真实故事)
  • 当你真的需要双关语时:stackoverflow.com/a/17790026/8120642

标签: c++ gcc strict-aliasing


【解决方案1】:

别名可以从字面上理解它的含义:当两个不同的表达式引用同一个对象时。类型双关是“双关”一个类型,即将某种类型的对象用作不同的类型。

形式上,类型双关语是未定义的行为,只有少数例外。当你不小心摆弄比特时,通常会发生这种情况

int mantissa(float f)
{
    return (int&)f & 0x7FFFFF;    // Accessing a float as if it's an int
}

例外情况是(简化的)

  • 访问整数作为它们的无符号/有符号对应物
  • charunsigned charstd::byte 访问任何内容

这被称为严格别名规则:编译器可以安全地假设两个不同类型的表达式永远不会引用同一个对象(上述例外除外),因为否则它们将具有未定义的行为。这有助于优化,例如

void transform(float* dst, const int* src, int n)
{
    for(int i = 0; i < n; i++)
        dst[i] = src[i];    // Can be unrolled and use vector instructions
                            // If dst and src alias the results would be wrong
}

gcc 说的是它稍微放宽了规则,并允许通过联合进行类型双关,即使标准不需要它

union {
    int64_t num;
    struct {
        int32_t hi, lo;
    } parts;
} u = {42};
u.parts.hi = 420;

这是类型双关语 gcc 保证将起作用。其他情况可能看起来有效,但有一天可能会默默地被打破。

【讨论】:

  • 我认为您的示例失败在于该结构中位字段的布局本身是实现定义的。 C 中对位域的糟糕定义是那些真正令人讨厌的事情之一,它可能为时已晚修复。双关语类型没问题(至少在 GCC 中),但位字段可能会也可能不会达到您的预期。
  • @DanMills Fair,但我想不出一个漂亮而简单的双关语。我想如果我想展示实际可行的方法,还不如一路走下去。
  • @PasserBy 一个比较常见的例子是union { long long x; struct { unsigned low, high } }(或相同,但使用unsigned[2],你懂的)。
  • 在 gcc/clang 对“严格别名”规则的解释之外的上下文中,术语“别名”不会用于描述使用一个引用派生另一个引用的情况,而新的引用用于访问对象,然后在对象以任何其他方式使用之前被放弃。
  • @PasserBy:两个引用在不相交的时间标识同一个对象这一事实并不意味着别名。引用用于派生另一个立即用于访问对象的引用也不是事实。这些都是预期访问模式,而术语“别名”是指可以看到引用的生命周期重叠但无法看到它们之间的任何关系的情况。
【解决方案2】:

术语是个好东西,我可以随心所欲地使用它,其他人也可以!

当它们具有相同的字节大小时,两种类型是否相似?如果不是,类似的类型有哪些?

粗略地说,当类型在常量或符号上有所不同时,它们是相似的。仅以字节为单位的大小肯定是不够的。

别名是类型相似的类型双关语的特定情况吗?

类型双关语是绕过类型系统的任何技术。

别名是一种特殊情况,涉及将不同类型的对象放置在同一地址。当类型相似时,通常允许使用别名,否则禁止使用别名。此外,可以通过char(或类似于char)左值访问任何类型的对象,但不允许做相反的事情(即通过不同类型的左值访问char 类型的对象)。 C 和 C++ 标准都保证了这一点,GCC 只是实现了标准要求的内容。

GCC 文档似乎在狭义上使用“类型双关语”来阅读除最后写入的工会成员之外的工会成员。即使类型不相似,C 标准也允许这种类型的双关语。 OTOH C++ 标准不允许这样做。 GCC 可能会也可能不会将权限扩展到 C++,文档对此并不清楚。

没有-fstrict-aliasing,GCC 显然放宽了这些要求,但具体到什么程度还不清楚。请注意,-fstrict-aliasing 是执行优化构建时的默认值。

底线,只是按标准编程。如果 GCC 放宽了标准的要求,那么它并不重要,也不值得麻烦。

【讨论】:

  • 标准的作者故意允许专门的实现以使其不适合大多数用途的方式运行。尽管-fstrict-aliasing 启用的优化中有 90% 以上在通用实现中是合理的,但其余 10%(虚假的“优化”)使该模式不适用于许多用途。
【解决方案3】:

在 ANSI C (AKA C89) 中,您有(第 3.3.2.3 节结构和联合成员):

如果在将值存储到对象的不同成员之后访问联合对象的成员,则行为是实现定义的

在 C99 中,您有(第 6.5.2.3 节结构和联合成员):

如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为对象表示中的对象表示6.2.6 中描述的新类型(有时称为“类型双关语”的过程)。这可能是一个陷阱表示。

IOW,C 中允许基于联合的类型双关语,尽管实际语义可能不同,具体取决于支持的语言标准(请注意,C99 语义比 C89 的实现定义的更窄)。

在 C99 中,您也有(第 6.5 节表达式):

对象的存储值只能由具有以下类型之一的左值表达式访问:

——与对象的有效类型兼容的类型,

——与对象的有效类型兼容的类型的限定版本,

——对象的有效类型对应的有符号或无符号类型,

— 有符号或无符号类型,对应于对象有效类型的限定版本,

——在其成员中包含上述类型之一的聚合或联合类型(递归地,包括子聚合或包含联合的成员),或

——一种字符类型。

C99 中有一节(6.2.7 兼容类型和复合类型)描述了兼容类型:

如果它们的类型相同,则两种类型具有兼容的类型。附加规则 类型说明符的 6.7.2 中描述了确定两种类型是否兼容, 在 6.7.3 中用于类型限定符,在 6.7.5 中用于声明符。 ...

然后(6.7.5.1 指针声明符):

对于要兼容的两种指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。

稍微简化一下,这意味着在 C 中,通过使用指针,您可以将有符号整数作为无符号整数访问(反之亦然),并且您可以访问任何内容中的单个字符。其他任何事情都会构成混叠违规。

您可以在各种版本的 C++ 标准中找到类似的语言。但是,据我所知,在 C++03 和 C++11 中,基于联合的类型双关语是不允许的(与 C 不同)。

【讨论】:

  • UV:这个答案阐明了“兼容类型”的概念(我想这就是他们所说的“相似类型”的意思)。我完全同意标准没有明确允许它,但它在某些情况下适用于 GCC。这是“未明确允许”并不意味着禁止的一种情况。
  • @L.C.这并不意味着它不会在不同的编译器、架构、操作系统甚至新的编译器版本上突然中断。
  • 你是对的,重点是……但是编写开源、灵活和可移植等的代码并不总是主要目标。这并不优雅,也不是一个好习惯,但有时人们只是想要一个在当前机器/操作系统上运行的二进制文件......所以如果编译器产生了符合预期的“有效”代码......为什么不呢!
【解决方案4】:

根据 C11 草案 N1570 中的脚注 88,“严格别名规则”(6.5p7)旨在指定编译器必须允许事物可能出现别名的情况,但没有尝试定义什么别名。在某个地方,出现了一种流行的看法,即规则定义的访问以外的访问代表“别名”,而允许的访问则不是,但实际上恰恰相反。

给定一个类似的函数:

int foo(int *p, int *q)
{ *p = 1; *q = 2; return *p; }

第 6.5p7 节没有说如果 pq 标识相同的存储,则它们不会别名。相反,它指定它们允许别名。

请注意,并非所有涉及将一种类型的存储访问为另一种类型的操作都表示别名。对从另一个对象新可见派生的左值的操作不会“别名”该另一个对象。相反,它对该对象的操作。如果在创建对某个存储的引用到使用它的时间之间,以某种方式引用了相同的存储不是从第一个派生,或者代码进入了发生这种情况的上下文,则会发生别名.

虽然识别左值何时派生自另一个左值的能力是实施质量问题,但标准的作者必须预期实施能够识别超出强制要求的某些构造。没有通过使用成员类型的左值访问与结构或联合关联的任何存储的一般权限,标准中也没有明确必须识别涉及someStruct.member的操作作为对someStruct 的操作。相反,标准的作者希望编译器编写者做出合理的努力来支持客户需要的结构,应该比委员会更好地判断这些客户的需求并满足它们。由于任何为识别派生引用做出了远程合理努力的编译器都会注意到someStruct.member 派生自someStruct,因此标准的作者认为没有必要明确规定这一点。

不幸的是,对结构的处理如下:

actOnStruct(&someUnion.someStruct);
int q=*(someUnion.intArray+i)

已经从“很明显,actOnStruct 和指针取消引用应该对someUnion(以及因此它的所有成员)起作用,没有必要强制执行这种行为”到“由于标准没有'不需要实现认识到上述操作可能会影响someUnion,任何依赖这种行为的代码都会被破坏并且不需要支持”。除了-fno-strict-aliasing 模式外,gcc 或 clang 都不可靠地支持上述任何一种构造,即使支持它们会阻止的大多数“优化”会生成“高效”但无用的代码。

如果您在任何具有此类选项的编译器上使用-fno-strict-aliasing,几乎任何东西都可以工作。如果您在 icc 上使用-fstrict-aliasing,它将尝试支持使用类型双关语而不使用别名的构造,尽管我不知道是否有任何文档说明它确实可以处理或不处理哪些构造。如果您在 gcc 或 clang 上使用 -fstrict-aliasing,那么任何有效的操作都纯属偶然。

【讨论】:

    【解决方案5】:

    我认为添加一个补充答案很好,只是因为当我问这个问题时,我不知道如何在不使用 UNION 的情况下满足我的需求:我很固执地使用它,因为它似乎准确地回答了我的需求。

    进行类型双关语和避免未定义行为的可能后果(取决于编译器和其他环境设置)的好方法是使用 std::memcpy 并将内存字节从一种类型复制到另一种类型。对此进行了解释 - 例如 - herehere

    我还读到,当编译器使用联合生成用于类型双关的有效代码时,它会生成与使用 std::memcpy 时相同的二进制代码。

    最后,即使这些信息不能直接回答我最初的问题,它也非常相关,我觉得在这里添加它很有用。

    【讨论】:

      猜你喜欢
      • 2014-06-11
      • 1970-01-01
      • 2018-08-05
      • 2019-09-01
      • 1970-01-01
      • 2011-10-01
      • 2012-06-14
      • 2011-07-18
      相关资源
      最近更新 更多