指定的初始化程序可以合法地引用它在C99中初始化的变量吗?

问题描述

GCC和Clang都允许指定的初始化程序引用正在初始化的struct或数组的成员,但这是合法且定义明确的行为吗?

以下代码示例针对GCC和Clang编译并运行,在两种情况下均输出{ .a = 3,.b = 6,}

#include <stdio.h>

typedef struct
{
    int a;
    int b;
} foo;

int main()
{
    foo bar = {
        .a = 3,.b = bar.a + 3,};
    printf("{ .a = %d,.b = %d,}\n",bar.a,bar.b);

    return 0;
}

GCC为指定的初始化生成以下输出Compiler Explorer link),该输出表明此示例的操作是安全的:

mov     dword ptr [rbp - 4],0
mov     dword ptr [rbp - 16],3
mov     eax,dword ptr [rbp - 16]
add     eax,3
mov     dword ptr [rbp - 12],eax

draft C99 spec的6.7.8节对此进行了讨论,但是我看不出它如何以一种或另一种方式定义这种行为。

特别是,第19点建议初始化以指定的顺序进行,但第23点提到具有未指定顺序的副作用。我不确定是否将写入该结构的数据视为副作用。

  1. 初始化应以初始化程序列表的顺序发生,为特定子对象提供的每个初始化程序都将覆盖先前为同一子对象列出的所有初始化程序;所有未显式初始化的子对象都应与具有静态存储持续时间的对象隐式初始化。
  1. 初始化列表表达式中发生副作用的顺序未指定

解决方法

您引用的是C标准的旧版本。当前的草稿(自C11起)针对第23点具有:

初始化列表表达式的求值相对于彼此不确定,因此未指定发生任何副作用的顺序。

我认为这意味着编译器可以选择在使用该特定表达式之前的任何时间评估特定的初始化表达式,这意味着它可能会在其所引用的元素初始化之前或之后发生。

在这种情况下,在初始化表达式中使用同一聚合对象的(可能)未初始化的元素必须导致不确定的值。

,

此脚注

  1. 尤其是,评估顺序不必与 子对象初始化的顺序

此报价

23初始化列表表达式的计算结果为 相对于彼此不确定地排序,因此 未确定发生副作用的顺序。152)

意味着这样的初始化

foo bar = {
        .a = 3,.b = bar.a + 3,};

调用未定义的行为,因为表达式bar.a + 3可以在初始化数据成员a之前求值。

尤其是未定义的行为定义为

可能的未定义行为范围从忽略情况开始 完全无法预测

,

给出类似的内容:

struct foo {int arr[10],*p;};

这样的定义没有问题:

struct foo x = {.p = x.arr,.arr={1,2,3,4,5}};

引用正在构造的对象,因为这种引用直到构造完成后才真正用于访问。在某些极端的情况下,尝试访问该对象会产生已定义的结果(例如,如果一个成员的初始化表达式作为副作用将其已经或将要初始化的值存储到另一成员),但是我不要以为该标准的作者就实质性细节考虑此类情况并确定是否应该对其进行定义。我认为很明显,与确定和写入结构成员的初始值有关的动作是相互不确定地排序的,这意味着如果某个结构成员被除初始化本身以外的其他动作访问,则此类动作可能在之前或之后发生。对该成员进行初始化之后,这种排序可能会带来任何后果。