【问题标题】:Is pointer arithmetic on inactive member of a union UB?联合UB的非活动成员的指针算术?
【发布时间】:2023-04-04 00:51:01
【问题描述】:

让我们考虑这个示例代码:

struct sso
{
    union {
        struct {
            char* ptr;
            char size_r[8];
        } large_str;
        char short_str[16];
    };

    const char* get_tag_ptr() const {
        return short_str+15;
    }
};

[basic.expr] 中指定,只要结果指向数组的另一个元素(或超过对象或最后一个元素的结尾),就允许使用指针运算。然而,如果数组是联合的非活动成员,则在此设置中没有指定会发生什么。我相信这不是问题short_str+15 绝不是 UB。对吗?

The following question 清楚地表明了我的意图

【问题讨论】:

  • IIRC 在您真正尝试取消引用结果指针之前,它不是 UB。
  • @Someprogrammerdude 不,指针算术本身会产生未定义的行为……例如,见尾后一指针的特殊情况(您可以计算但不能取消引用) .当然,这种迂腐的 UB 永远不会给你带来麻烦,但这个问题被标记为“语言律师”。
  • 但是,在此基础上,您是说当成员处于活动状态时获取的指针在不活动时变为 UB(我可以忍受)并在回到活动范围时保持 UB?老实说,我发现编译器可能会将联合优化为单个单元以外的任何东西的整个想法令人担忧。
  • 你应该使用std::variant而不是原始联合。
  • @GemTaylor 我是泛泛而谈,而不是专门针对工会。但请记住,UB 是关于行为,而不是。取消引用指向有效对象的指针很好,无论在其他点取消引用它是否会产生 UB。

标签: c++ c++11 language-lawyer pointer-arithmetic


【解决方案1】:

编写return short_str+15;,您获取一个对象的地址,该对象的生命周期可能尚未开始,但这不会导致未定义的行为,除非您取消引用它。

[basic.life]/1.2

如果对象是联合成员或其子对象, 它的生命周期仅在该联合成员被初始化时才开始 工会成员,或如[class.union] 中所述。

[class.union]/1

在联合中,一个非静态数据成员是活动的,如果它的 name 指的是生命周期已经开始但尚未结束的对象 ([basic.life])。至多一个对象的非静态数据成员 union 类型可以随时激活,也就是最多的值 其中一个非静态数据成员可以存储在任意位置的联合中 时间。

但是

[basic.life]/6

在对象的生命周期开始之前但在对象的存储之后 对象将占用已被分配,或者,在一个生命周期之后 对象已经结束并且在对象占用的存储空间之前 重用或释放,任何表示该地址的指针 可以使用对象将要或曾经所在的存储位置 但仅限于有限的方式。对于正在建造的物体或 破坏,见[class.cdtor]。否则,这样的指针指向已分配 存储([basic.stc.dynamic.allocation]),并像使用指针一样使用指针 类型 void* ,定义明确。通过这样的指针间接是 允许,但生成的左值只能以有限的方式使用, 如下所述。
- [列表与工会无关]

【讨论】:

  • 我同意这一切。可能还想从 class.union 中提到“每个非静态数据成员都被分配,就好像它是结构的唯一成员一样”,这意味着所有联合成员的存储即使在它们不活动时也被分配。
  • 所以也许你想回答这个related question
  • 你确定short_str+15 确实“使用指针就像指针是void* 类型一样”?
  • short_str+15 的结果取决于short_str 的类型,所以我不认为它使用“好像指针是void* 类型”。
  • @xskxzr 经过一段时间的三思,short_str+15 取决于 short_strstatic 类型,这是一个已知的编译时信息,不受运行时概念,例如它所指向的对象的生命周期。
【解决方案2】:

联合成员上的指针算术是否会导致别名取决于指针最终将如何使用。在补充标准的实现上,保证“类型访问”规则将仅在存在实际别名的情况下应用,或者(对于 C++)在涉及具有非平凡语义的类型的情况下,指针操作的有效性与它们是在活动成员还是非活动成员上执行的关系不大。

考虑,例如:

#include <stdint.h>

uint32_t readU(uint32_t *p) { return *p; }
void writeD(double *p, double v) { *p = v; }

union udBlob { double dd[2]; uint32_t ww[4]; } udb;

uint32_t noAliasing(int i, int j)
{
  if (readU(udb.ww+i))
    writeD(udb.dd+j, 1.0);
  return readU(udb.ww+i);
}

uint32_t aliasesUnlessDisjoint(int i, int j)
{
  uint32_t *up = udb.ww+i;
  double *dp = udb.dd+j;

  if (readU(up))
    writeD(dp, 1.0);
  return readU(up);
}

在 readU 执行期间,不会通过任何其他方式访问通过 *p 访问的存储,因此在执行该函数期间没有别名。同样在writeD 的执行过程中。在noAliasing 的执行期间,所有将影响与udb 关联的存储的所有操作都是使用从udb 派生的指针执行的,并且显然具有明显不重叠的活动生命周期,因此那里没有别名。

aliasesUnlessDisjoint的执行过程中,所有的访问都是使用派生自udb的指针进行的,但是在dp的创建和使用之间通过up访问存储,通过@987654332访问存储@在up的创建和使用之间。因此,*dp*up 将在 aliasesUnlessDisjoint 执行期间出现别名,除非 udb.ww[i]udb.dd[j] 占用不相交的存储空间。

请注意,gcc 和 clang 都应用类型访问规则,即使在上面的无别名函数这样没有实际别名的情况下也是如此。尽管标准明确规定someArray[y] 形式的左值表达式等同于*(someArray+(y)),但如果使用[] 语法,gcc 和clang 将只允许可靠访问联合中的数组成员。例如:

uint32_t noAliasing2(int i, int j)
{
  if (udb.ww[i])
    udb.ww[j] = 1.0;
  return udb.ww[i];
}
uint32_t noAliasing3(int i, int j)
{
  if (*(udb.ww+i))
    *(udb.dd+j) = 1.0;
  return *(udb.ww+i);
}

虽然 gcc 或 clang 为noAliasing2 生成的代码在对udb.dd[j] 进行操作后会重新加载udb.ww[i],但noAliasing3 的代码不会。根据标准,这在技术上是允许的(因为所写的规则不允许在任何情况下访问udb.ww[i]!),但这绝不意味着对作者认为 gcc 和 clang 的行为适用于高质量的实现。纯粹看标准,我看不出任何特定的 noAliasing 形式应该或多或少比任何其他形式有效,但考虑在 -fstrict-aliasing 模式下使用 gcc 或 clang 的程序员应该认识到 gcc 和 clang区别对待。

【讨论】:

  • 这很有趣,我没注意。为了公平起见,我们记录了这种行为 (gcc documentation)
  • @Oliv:恕我直言,标准应该为 gcc 和 clang 等编译器识别一个单独的实现类别,它要求使用任何特定非字符类型访问的任何存储都不能用任何其他类型写入类型,也不与任何其他非字符类型一起读取,从而减轻 gcc 和 clang 试图遵守有效类型规则的不可行的极端情况的负担,但同时认识到 gcc 和 clang 不能的访问模式的合法性支持,但其他编译器可以(即使在-fstrict-aliasing 模式下)。
  • @Oliv:我认为当一些作者编写不涉及别名的“潜在不工作”代码示例时,gcc 的轮子掉了下来,即使支持非别名情况本来是微不足道的.这导致采用中间表示,过滤掉除noAliasing2之外的所有上述之间的差异,因此将其他两种形式视为等同于aliasesUnlessDisjoint
猜你喜欢
  • 2019-10-11
  • 2021-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-20
  • 2015-04-18
  • 2019-03-30
相关资源
最近更新 更多