【问题标题】:Is it undefined behavior to return an uninitialized, ultimately unused, struct?返回一个未初始化的、最终未使用的结构是未定义的行为吗?
【发布时间】:2018-06-08 00:03:16
【问题描述】:

如果唯一的后续使用是在如下所示的初始化语句中,那么是否UB返回一个结构而不初始化它:

typedef struct { int x; } s;

s callee(void) {
  s ret;
  return ret;
}

void caller() {
  s dummy = callee();
}

【问题讨论】:

  • 您似乎认为struct 是否未使用很重要;编译器无法确定的情况怎么办?
  • @ScottHunter - 我不知道该结构未使用是否重要,这就是我问的原因。显然,在很多情况下编译器无法确定这一点,例如 calleecaller 是否位于单独的编译单元中 - 但我不确定你在这里得到什么。我不是在问“如果编译器可以证明它是未使用的,它是否是 UB 返回一个 uninit 结构”-我是在问上面的模式是否是 UB。例如,return s 可能是 UB(无论调用者如何),或者 s dummy = callee() 可能是 UB,也可能不是。
  • 注意:某事物是否是 UB 是源的独立属性,而 not 取决于编译器能证明或不能证明什么,或者您使用什么编译器,甚至是否存在任何编译器。
  • 在 C++ 中,这是未定义的行为。 C++ 允许复制窄字符类型的不确定值(创建新的不确定值),但对于任何其他类型,左值到右值的转换(包括按值返回)会产生未定义的行为。
  • @BeeOnRope:可能还没有任何UB,但是在不触发UB的情况下不能以任何方式使用返回值。这不是发生的事情(至少在 C++ 中),但考虑它是合理的。这在其他情况下确实会发生,例如返回一个悬空引用。

标签: c language-lawyer c11


【解决方案1】:

首先考虑这个类似的代码:

s ret;
s dummy = ret;

结构不能有陷阱表示 (C11 6.2.6.1/6)。但是由于 Itanium 子句 (C11 6.3.2.1/2) 的原因,此代码会导致未定义的行为,该子句说使用从未被占用其地址的未初始化自动对象的值会导致 UB。

所以这段代码会很好定义:

s ret;
&ret;
s dummy = ret;

有关该条款的进一步阅读,请参阅:Is a^a or a-a undefined behaviour if a is not initialized?


对于具有函数返回值的版本:标准中没有说明返回值是否算作安腾条款中的自动对象。我倾向于说它没有,因为标准没有将返回值描述为对象。但是,如果熟悉 Itanium ABI 的人能够评论通过返回值传递未初始化的结构是否会触发 NaT 异常,那就太好了。

取而代之的是,我的立场是函数调用版本与上面讨论的赋值版本具有相同的语义,即发布的代码是 UB,但添加 &ret; 使其定义明确。

【讨论】:

  • 出于好奇,是什么让您将 6.3.2.1/2 称为“安腾条款”?我认为与特定的英特尔设备有关吗?
  • @Lundin 是的,see here
  • 请注意,我测试过的所有编译器都将 J.2 中的更强规则(“如果...使用不确定的值,则行为未定义”)视为 100% 规范,即使它出现在一个内容丰富的附件中,据我所知,规范性文本并没有那么远。我深思熟虑后认为,更强有力的规则确实反映了委员会的意图。 (不同的是,获取地址本身并没有任何区别,类型是否有陷阱表示也无关紧要。)
  • @Lundin:在 Itanium 上,每个 64 位寄存器可以有 2^64+1 个状态:它可以保存 2^64 个值中的一个,也可以保存一个特殊的“not-a” -值”状态。如果代码尝试例如,Itanium 将生成一个陷阱。当寄存器处于“非值”状态时对其执行算术运算,出于某种原因,人们似乎认为这就是使用未初始化的自动变量调用 UB 的原因。
【解决方案2】:

TLDR:虽然函数“通过”调用者最终忽略的不确定值的能力提供了在大多数平台上远远超过成本的好处,因此应该由针对此类平台的高质量实现提供,但标准不要求提供它的实现,因此“聪明”的实现不提供。


在编写标准时,所有编译器都会一致地处理许多结构,但标准没有明确定义。标准指出,在标准不强加要求的情况下,处理行动的一种常见方式是“以环境特征的书面方式”,但在基本原理中指出,何时这样做的决定是实施质量问题,而不是一个一致性。基本原理还承认,一个实现可能是符合标准的,但质量很差以至于没有用,但作者认为没有必要花费精力来禁止这样的实现。

典型平台的高质量编译器可以通过两种明智且有用的方式处理上述代码:

  1. 如果函数试图返回未初始化的对象,编译器可能会陷入实现定义的方法,而不管调用代码是否会使用该值 [生成陷阱的代码可能没有办法对调用代码一无所知]。

  2. 编译器可以将一些任意位集合留在调用者可能使用或不使用的某个地方,如果调用者实际上不使用它们,则不会产生副作用。请注意,如果某种类型的对象在内存中存储时没有填充位,则它们在存储在寄存器中时可能具有填充位,并且如果这些填充位设置不正确,则可能会出现异常。

标准的作者没有试图列举一个实现在处理不确定值时可能做的所有事情,因为他们认为寻求产生高质量实现的实现者将根据预期的目标决定最合适的行动方案相关实现的平台和目的。

不幸的是,即使在任何通用平台上,在调用者将要忽略它们的情况下让函数安全地返回不确定值基本上不会花费任何成本,并且在某些情况下这样做会产生比其他方式更有效的代码(例如说给定:

extern volatile int vv1, vv2, vv3;
int foo(int mode)
{
  int result;
  vv1 = 1;
  if (mode & 1)
    result = vv2;
  if (mode & 2)
    result = vv3;
  return result;
}

语句“foo(0);”将 1 存储到 vv1 而没有其他副作用 可以避免程序员强制编译器生成不必要的负载)对于编译器编写者来说,寻找“聪明”的方法来利用标准没有的事实已经成为一种时尚需要这样的保证。例如,上面的代码可能被“优化”为:

int foo(int mode)
{
  vv1 = 1;
  if (!(mode & 2))
    return vv2;
  if (mode & 1)
    +vv2; // Access and ignore value
  return vv3;
}

无论这种“优化”的实际价值是否会超过让程序员允许编译器避免不必要的负载的实际价值,无法确定他们的代码只会在高质量实现上运行的程序员需要考虑“聪明的”。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-28
    • 1970-01-01
    • 2021-03-27
    • 2015-04-19
    • 1970-01-01
    • 2021-12-21
    相关资源
    最近更新 更多