【问题标题】:Is it a strict aliasing violation to alias a struct as its first member?将结构别名为其第一个成员是否是严格的别名违规?
【发布时间】:2018-05-17 04:06:17
【问题描述】:

示例代码:

struct S { int x; };

int func()
{
     S s{2};
     return (int &)s;    // Equivalent to *reinterpret_cast<int *>(&s)
}

我认为这是常见的,并且被认为是可以接受的。该标准确实保证结构中没有初始填充。但是这种情况并没有在严格的别名规则(C++17 [basic.lval]/11)中列出:

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

  • (11.1) 对象的动态类型,
  • (11.2) 对象动态类型的 cv 限定版本,
  • (11.3) 与对象的动态类型类似(如 7.5 中定义)的类型,
  • (11.4) 对象的动态类型对应的有符号或无符号类型,
  • (11.5) 有符号或无符号类型,对应于对象动态类型的 cv 限定版本,
  • (11.6) 一种聚合或联合类型,在其元素或非静态数据成员(递归地包括子聚合或包含联合的元素或非静态数据成员)中包含上述类型之一,李>
  • (11.7) 是对象动态类型的(可能是 cv 限定的)基类类型,
  • (11.8) char、unsigned char 或 std::byte 类型。

似乎很明显,对象s 正在访问其存储的值。

项目符号中列出的类型是进行访问的泛左值的类型,而不是正在访问的对象的类型。在这段代码中,glvalue 类型是int,它不是聚合或联合类型,排除了 11.6。

我的问题是:这段代码是否正确,如果正确,在上述哪个要点下是允许的?

【问题讨论】:

  • 我主要熟悉 C 标准而不是 C++ 标准,但前者的作者认为没有必要指定 any 左值的情况聚合成员类型实际上可用于访问聚合。即使像myStruct.member=23; 这样的东西也会调用UB,除非member 具有字符类型,但是编译器必须相当迟钝才能识别出这种用法。编译器同样必须是迟钝的,无法识别新转换为成员类型的指针用于访问该成员的情况。然而,标准......
  • ...不强制要求这种行为,但依赖于编译器编写者认识到即使在标准未强制要求的情况下,质量实现也应该表现得有用。不幸的是,这种依赖被证明是错误的。

标签: c++ language-lawyer strict-aliasing reinterpret-cast


【解决方案1】:

演员的行为归结为 [expr.static.cast]/13;

“指向cv1 void的指针”类型的纯右值可以转换为“指向cv2 T的指针”类型的纯右值,其中T是一个对象类型,并且 cv2 的 cv 限定与 cv1 相同或更高。如果是原 指针值表示内存中一个字节的地址A,而A不满足T的对齐要求,则生成的指针值未指定。 否则,如果原始指针值指向对象a,并且存在类型为T(忽略cv-qualification)的对象b,则pointer-interconvertiblea,结果是指向b的指针。否则,指针值通过转换保持不变。

pointer-interconvertible的定义是:

如果满足以下条件,两个对象 a 和 b 是指针可互转换的:

  • 它们是同一个对象,或者
  • 一个是联合对象,另一个是该对象的非静态数据成员,或者
  • 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据成员,则该对象的第一个基类子对象,或
  • 存在一个对象 c,使得 a 和 c 可以指针互转换,而 c 和 b 可以指针互转换。

所以在原始代码中,ss.xpointer-interconvertible,因此 (int &amp;)s 实际上指定了 s.x

所以,在严格的别名规则中,正在访问其存储值的对象是s.x而不是s,所以没有问题,代码是正确的。

【讨论】:

  • 但是你没有做static_cast 并且这里没有“指向cv void的指针”?我不明白这有什么关系。
  • 基本上,划掉 static.cast 引用,并在您在 basic.compound 中引用的块之后包含非常重要的句子
  • @Barry reinterpret_cast 在不相关的指针类型之间被定义为两个static_casts 通过void *s 的序列
  • 注意结果是指向b的指针表示对象是b,因此(int*)&amp;s的指针指向int类型的x,所以取消引用(int*)&amp;s 是对象s 中的x,它的动态类型是int,所以通过glvaue int&amp; 访问值是合法的
  • @M.M 您的分析非常适合指针的情况,但我还在 [expr.reinterpret.cast] 中找到了一句话,即:T1 类型的左值表达式可以转换为如果可以使用 reinterpret_cast 将“指向 T1 的指针”类型的表达式显式转换为“指向 T2 的指针”类型,则键入“对 T2 的引用”。 结果引用与源 glvalue 相同的对象,但具有指定的类型。请注意强调的部分。似乎在您的示例中结果仍然引用 S 类型的对象,因为源 glvalue 是 s
【解决方案2】:

我认为是expr.reinterpret.cast#11

类型 T1 的泛左值表达式,指定对象 x,可以强制转换 如果是“指向 T1 的指针”类型的表达式,则指向类型 “对 T2 的引用” 可以使用 a 显式转换为类型“指向 T2 的指针” reinterpret_cast。结果是*reinterpret_­cast&lt;T2 *&gt;(p) 其中p 是指向x 的指针,类型为“指向T1 的指针”。没有暂时的 创建,没有复制,也没有构造函数或 转换函数称为 [1]

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

支持@M.M 关于pointer-incovertible的回答:

来自cppreference

假设满足对齐要求,reinterpret_cast 可以 少数限制情况之外更改指针的值 处理 pointer-interconvertible 对象:

struct S { int a; } s;


int* p = reinterpret_cast<int*>(&s); // value of p is "pointer to s.a" because s.a
                                     // and s are pointer-interconvertible
*p = 2; // s.a is also 2

struct S { int a; };

S s{2};
int i = (int &)s;    // Equivalent to *reinterpret_cast<int *>(&s)
                     // i doesn't change S.a;

【讨论】:

  • 该文本似乎只是定义(int &amp;)s 表示*reinterpret_cast&lt;int *&gt;(&amp;s),但它没有进一步解释后者如何与严格的别名规则交互
  • 您确实为我指明了正确的方向,以找到我认为正确的答案;谢谢
  • @MM,你是对的。也感谢您提供 pointer-inconvertible 解释。我也添加了一个参考来支持您的主张。
【解决方案3】:

所引用的规则源自 C89 中的类似规则,除非有人扩展了“by”一词的含义,或者在编写 C89 时认识到“未定义行为”的含义,否则该规则将是荒谬的。给定 struct S {unsigned dat[10];}s; 之类的东西,s.dat[1]++; 语句显然会修改 s 的存储值,但该表达式中唯一的 struct S 类型的左值仅用于生成 unsigned* 类型的值.用于修改任何对象的唯一左值类型为int

在我看来,有两种相关的方法可以解决这个问题:(1) 认识到标准的作者希望允许一种类型的左值明显地从另一种类型派生的情况,但没有不想纠结于必须考虑什么形式的可见派生的细节,特别是因为编译器需要识别的案例范围会根据他们执行的优化风格和使用它们的任务而有很大差异; (2) 认识到标准的作者没有理由认为,如果每个人都清楚有理由不这样做的话,标准是否真的要求对特定结构进行有效处理是重要的。

我认为委员会成员对于编译器是否给出以下内容没有达成共识:

struct foo {int ct; int *dat;} it;
void test(void)
{
  for (int i=0; i < it.ct; i++)
    it.dat[i] = 0;
}

应该被要求确保例如在it.ct = 1234; it.dat = &amp;it.ct; 之后,对test(); 的调用将使it.ct 归零并且没有其他效果。部分基本原理表明至少有一些委员会成员会期望如此,但是省略任何允许使用成员类型的任意左值访问结构类型对象的规则则表明并非如此。 C 标准从来没有真正解决过这个问题,C++ 标准稍微清理了一些东西,但也没有真正解决它。

【讨论】:

  • 您的示例中的代码显然是合法的:我们通过int 类型的左值(即it.dat[i])访问int 类型的对象的存储值(即it.ct) )。
  • 在我看来,问题是通过如下代码出现的:struct bar { int x,y; } b1, b2; int *p; void test2(void) { p = &amp;b2.y; *p = 17; b1 = b2; }。如果我们不允许使用int 类型的对象访问b2,那么这表明赋值b1 = b2 可以向上移动,我们可能不会以b1.y == 17 结束。但这种逻辑也表明b2.y = 17; 本身就是未定义的行为,这是荒谬的。
  • @NateEldredge:在编写标准时,“未定义行为”一词旨在“识别符合语言扩展的区域”。因此,委员会没有做出任何真正的努力来避免将其描述为 UB 操作,他们预计实施通常会以相同的方式处理。我认为很明显,您的示例应该可以预见地工作,因为 p 是在新可见从 b2 派生时使用的,但是如果要编写类似的东西,例如int test2(struct bar *p1, int *p2) { p1-&gt;x=1; *p2=3; return p1-&gt;x; }...
  • ...在p2struct bar 之间没有任何可见的关系,我认为编译器是否应该允许p2 别名bar-&gt;x 的可能性并不明确。在代码使用指针访问结构成员的绝大多数情况下,指针的派生及其所有使用将在没有任何通过其他方式访问或寻址结构的该部分的干预操作的情况下发生。由于编译器何时识别出新派生的指针的问题将被视为实现质量问题......
  • ...在标准的管辖范围之外,让标准允许使用新近可见的派生指针来访问对象几乎没有什么意义。对于任何不迟钝的人来说,很明显,可以看到指针是从另一种类型派生的高质量编译器应该允许它可能访问该类型的对象,但要求这样做毫无意义识别这种派生的编译器会尊重它,而不要求编译器识别这种派生。
猜你喜欢
  • 1970-01-01
  • 2020-04-11
  • 1970-01-01
  • 2017-02-25
  • 1970-01-01
  • 2016-02-24
  • 1970-01-01
相关资源
最近更新 更多