是否使用未定义所有成员的结构未定义?

时间:2017-11-22 11:02:20

标签: c struct language-lawyer undefined-behavior

在块范围内考虑此代码:

struct foo { unsigned char a; unsigned char b; } x, y;
x.a = 0;
y = x;

C [N1570] 6.3.2.1 2说“如果左值指定了一个可以用寄存器存储类声明的自动存储持续时间的对象(从未使用过它的地址),那么对象未初始化(未使用初始化程序声明,并且在使用之前未对其进行任何分配),行为未定义。“

虽然已为x的成员分配了值,但未对x执行任何分配,并且尚未对其进行分配。因此,看起来6.3.2.1 2告诉我们xy = x的行为未定义。

但是,如果我们为x的每个成员分配了一个值,那么为了6.3.2.1 2的目的,考虑x未初始化似乎是不合理的。

(1)标准中是否有任何内容严格来说导致6.3.2.1 2不适用于(未定义)上述代码?

(2)假设我们正在修改标准或确定对6.3.2.1 2的合理修改,是否有理由偏好以下其中一项而不是其他? (a)6.3.2.1 2不适用于结构。 (b)如果一个结构的至少一个成员被赋予了一个值,则该结构在6.3.2.1的目的下不是未初始化的。(c)如果一个结构的所有命名的 1 成员都已经为了6.3.2.1的目的,该结构没有被初始化。

脚注

1 结构可能有未命名的成员,因此并不总是可以为结构的每个成员分配值。 (即使结构初始化,未命名的成员也有不确定的值,每6.7.9 9。)

4 个答案:

答案 0 :(得分:8)

我的观点是,它是未定义的行为,因为它没有被标准明确定义。从4一致性§2(强调我的):

  

......否则是未定义的行为   在本国际标准中用'未定义的行为''或由   省略任何明确的行为定义

在N1570草案中多次读取之后,我找不到使用部分初始化的结构的行为的任何明确定义。一方面6.3.2.1§2说:

  

...如果   左值指定了一个自动存储持续时间的对象   声明与寄存器存储类(从未有过其地址)和该对象   未初始化(未使用初始化程序声明,并且没有对其进行任何分配)   在使用之前执行),行为未定义

所以这里x是自动的,永远不会被初始化(只有其中一个成员),而且从来没有采用它的地址,所以我们可以认为它是明确的UB

另一方面,6.2.6.1§6说:

  

...结构或联合对象的值永远不是陷阱表示,即使结构或联合对象的成员的值可能是陷阱表示。

正如6.2.6.1§5刚刚定义了一个陷阱表示:

  

某些对象表示不需要表示对象类型的值。如果存储   对象的值具有这样的表示,并由左值表达式读取   没有字符类型,行为是未定义的。如果产生这样的表示   副作用通过左值表达式修改对象的全部或任何部分,该表达式表示a成员的0值和b成员的未定义值。   没有字符类型,行为是未定义的.50)这样的表示被调用   陷阱表示。

我们可以认为获取结构的值总是合法的,因为它不能成为陷阱表示

此外,我不清楚设置结构成员的值是否实际上使结构处于单元化状态。

由于所有这些原因,我认为标准没有明确定义行为应该是什么,仅仅因为这个原因它是未定义的行为。

话虽如此,我很确定任何常见的编译器都会接受它并给y当前的x表示,这意味着a成员的0值和不确定的值与x.b成员b的当前表示相同的表示形式。

答案 1 :(得分:1)

首先,让我们注意6.3.2.1/2的引用部分,即所谓的" Itanium子句"是此代码可能出现问题的唯一条款。换句话说,如果没有这个条款,代码就可以了。结构可能没有陷阱表示,因此即使y = x;完全未初始化,x也可以。 DR 451的解决方案阐明了不确定的值可以通过赋值传播,而不会导致UB。

返回到此处的Itanium子句。正如您所指出的那样,标准没有明确说明x.a = 0;是否否定前提条件" x未初始化"。

IMO,这意味着我们应该转向Itanium子句的基本原理来确定意图。一般而言,标准文件的措辞的目的是实现意图;一般来说,我并不同意对标准的细节进行教条化:从创作措辞的人不想要的措辞中删除意义。

This Q/A对理由给出了很好的解释。潜在的问题是x可能存储在NaT位置1的寄存器中,然后y = x会因读取具有该位设置的寄存器而导致硬件异常。

所以问题是:在IA64上x.a = 0;清除NaT位?我不知道,我想我们需要一个熟悉该平台的人在这里给出一个确定的答案。

天真地,我想如果x在寄存器中,那么一般来说,x.a = 0;需要读取旧值,并应用掩码来清除a的位,如果xNaT,则触发例外。但是,x.a = 0;无法触发UB,因此逻辑必须不正确。也许IA64编译器永远不会在一个寄存器中存储一个结构,或者它们可能会在声明一个结构时清除NaT位,或者可能有一个硬件指令在先前的NaT寄存器上实现x.a = 0;,我不会&#39不知道。

答案 2 :(得分:1)

复制部分编写的结构属于质量实现将以一致的方式处理的操作类别,如果没有这样做的充分理由,则专用实现可能会有不同的处理方式,因为它们有充分的理由这样做,质量差但合规的实现可能会成为无理取闹的借口。

请注意,复制自动持续时间或由malloc创建的字符数组的未初始化值将属于类似的操作类别,除了会捕获此类操作的实现(例如,帮助程序员识别和跟踪潜在的信息泄漏) )将其描述为“符合标准”。

专门用于诊断意外信息泄漏的实现可能明智地限制了复制部分编写的结构的工作。在使用某种类型的单位化值可能导致奇怪行为的实现中,明智的做法是先复制具有类型的单位化成员的结构,然后尝试使用该副本的成员。 / p>

该标准没有特别说明部分编写的结构是否被视为已编写,因为寻求产生高质量实现的人们不在乎。专门用于检测潜在信息泄漏的质量实现应在尝试复制未初始化的数据时大惊小怪,而不必考虑标准何时或不允许这种行为(前提是它们将自己描述为不符合标准)。设计用于支持各种程序的高质量通用实现应允许在程序不在整个结构复制的上下文之外查看未初始化部分的情况下复制部分初始化的结构(这种处理非常有用,通常会产生成本在没有人为的情况下则没有任何内容)。该标准可以被解释为授予质量差但合规的实现正确地将复制部分编写的结构视为行为不合理的借口,但是此类实现几乎可以使用任何东西作为借口。除非有充分的理由证明复制结构,否则质量实现不会有任何异常。

答案 3 :(得分:-3)

C标准规定结构类型不能有陷阱表示,尽管结构的成员可以。保证有用的主要情况是涉及部分书面结构的案件。此外,禁止在编写所有成员之前复制结构,即使是副本的接收者永远不会使用的,也需要程序员编写不必要的低效代码并且没有用处。以“优化”的名义强加这样的要求将是彻头彻尾的愚蠢,我知道没有证据表明该标准的作者打算这样做。

不幸的是,标准的作者使用相同的术语来描述两种情况:

  1. 有些实现在所有情况下都定义了某个动作X的行为,而有些实现仅为某些动作定义了行为X.标准的其他部分在几个选择的案例中定义了行动。作者想要说实现不需要像在所有情况下定义行为的行为一样,没有撤销在标准中其他地方做出的保证

  2. 虽然标准的其他部分在某些情况下会定义操作X的行为,但保证所有这些情况下的行为可能很昂贵,并且不需要实现来保证它们甚至其他部分标准将定义它们

  3. 在编写标准之前,某些实现会将所有自动变量归零。因此,这些实现将保证读取未初始化值的行为,即使是具有陷阱表示的类型。该标准的作者希望明确表示他们不希望所有实施同样如此。此外,一些对象可以在存储在存储器中时定义所有位模式的行为,但是当存储在寄存器中时不定义。然而,这种处理通常会限制为标量类型,而不是结构。

    从实际角度来看,定义将结构复制为复制所有字段的状态(已定义或不确定)的行为,除了允许编译器在复制部分编写的结构时以任意方式运行时,其成本也不会高。不幸的是,一些编译器编写者错误地认为“聪明”和“愚蠢”是反义词,因此表现得好像标准的作者希望邀请编译器假设程序永远不会收到任何会导致结构被复制的输入部分写了。