何时以及如何在 sizeof 表达式中计算 VLA?

chq*_*lie 5 c c99 expression-evaluation language-lawyer variable-length-array

C 标准有这种语言:

6.5.3.4 sizeof 和 _Alignof 运算符

语义学

  1. sizeof运算符产生其操作数的大小(以字节为单位),该操作数可以是表达式或带括号的类型名称。大小由操作数的类型确定。结果是一个整数。如果操作数的类型是变长数组类型,则对操作数求值;否则,不计算操作数并且结果是整型常量。

我不清楚标准的含义:如果操作数的类型是可变长度数组类型,则对操作数进行求值

  • 如果操作数的类型是可变长度数组类型,则评估参数似乎没有任何目的,因为可以根据类型的定义确定大小,如 6.7.6.2 数组声明符中规定那样可变长度数组类型的每个实例的大小在其生命周期内不会改变。
  • 另一方面,如果操作数是带括号的可变长度数组类型的名称,例如在sizeof(char[foo()])大小表达式中必须在运行时求值来计算大小,但标准的语言似乎没有涵盖这种情况(什么是类型名称的类型吗?)

C 标准的语言是否应该修改以澄清?

下面是一个测试程序,用于说明 VLA 的某些特定情况下的行为:

#include <stdio.h>

static int N = 0;
int foo(void) { return ++N; }

int main() {
    typedef char S[foo()];      // foo() is called
    printf("typedef char S[foo()];\t");                             printf("N=%d\n", N);
    printf("sizeof(S)=%d\t\t", (int)sizeof(S));                     printf("N=%d\n", N);

    typedef char U[foo()];      // foo() is called
    printf("typedef char U[foo()];\t");                             printf("N=%d\n", N);
    printf("sizeof(U)=%d\t\t", (int)sizeof(U));                     printf("N=%d\n", N);

    S s1;
    printf("S s1;\t\t\t");                                          printf("N=%d\n", N);
    printf("sizeof(s1)=%d\t\t", (int)sizeof(s1));                   printf("N=%d\n", N);

    S s2;
    printf("S s2;\t\t\t");                                          printf("N=%d\n", N);
    printf("sizeof(s2)=%d\t\t", (int)sizeof(s2));                   printf("N=%d\n", N);

    U u1;
    printf("U u1;\t\t\t");                                          printf("N=%d\n", N);
    printf("sizeof(u1)=%d\t\t", (int)sizeof(u1));                   printf("N=%d\n", N);

    U *pu1 = &u1;
    printf("U *pu1 = &u1;\t\t");                                    printf("N=%d\n", N);
    printf("sizeof(*pu1)=%d\t\t", (int)sizeof(*pu1));               printf("N=%d\n", N);

    U *pu2 = NULL;
    printf("U *pu2 = NULL;\t\t");                                   printf("N=%d\n", N);
    // sizeof(*pu2) does not evaluate *pu2, contrary to the Standard specification
    printf("sizeof(*pu2)=%d\t\t", (int)sizeof(*pu2));               printf("N=%d\n", N);

    char x2[foo()][foo()];      // foo() is called twice
    printf("char x2[foo()][foo()];\t");                             printf("N=%d\n", N);
    printf("sizeof(x2)=%d\t\t", (int)sizeof(x2));                   printf("N=%d\n", N);
    printf("sizeof(x2[0])=%d\t\t", (int)sizeof(x2[0]));             printf("N=%d\n", N);

    // sizeof(char[foo()]) evaluates foo()
    printf("sizeof(char[foo()])=%d\t", (int)sizeof(char[foo()]));   printf("N=%d\n", N);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出(clang 和 gcc):

typedef char S[foo()];  N=1
sizeof(S)=1             N=1
typedef char U[foo()];  N=2
sizeof(U)=2             N=2
S s1;                   N=2
sizeof(s1)=1            N=2
S s2;                   N=2
sizeof(s2)=1            N=2
U u1;                   N=2
sizeof(u1)=2            N=2
U *pu1 = &u1;           N=2
sizeof(*pu1)=2          N=2
U *pu2 = NULL;          N=2
sizeof(*pu2)=2          N=2
char x2[foo()][foo()];  N=4
sizeof(x2)=12           N=4
sizeof(x2[0])=4         N=4
sizeof(char[foo()])=5   N=5
Run Code Online (Sandbox Code Playgroud)

Joh*_*ode 2

如果操作数的类型是可变长度数组类型,则评估参数似乎没有任何目的,因为可以根据类型的定义确定大小,如 6.7.6.2 数组声明符中规定那样可变长度数组类型的每个实例的大小在其生命周期内不会改变。

但只有在运行时实例化该数组后,该大小才能得知。某种评估必须在运行时执行。没有具体说明该评估到底需要什么。

C 标准的语言是否应该修改以澄清?

我想是的,是的。我认为以下习惯用法对于动态分配二维数组非常有用,其中行数和列数直到运行时才知道:

int rows, cols;
...
T (*arr)[cols] = malloc( sizeof *arr * rows );
Run Code Online (Sandbox Code Playgroud)

然而,正如标准目前的措辞一样,这(很可能)会调用未定义的行为,因为我正在*arr运行时进行评估,但arr此时尚未初始化(并且很可能无效)。您不需要取消引用 arr来获取数组类型的大小,但不幸的是标准中的语言不是那么精细。我希望看到类似于“如果操作数的类型是可变长度数组类型,则对操作数进行评估以单独获取数组大小”的语言。

  • 这只是对标准部分内容的足够迂腐的阅读会被描述为未定义行为操作的许多情况之一,这些操作具有一种含义,否则可以通过阅读标准的其他部分和平台文档来推断。该标准的作者期望编译器编写者在没有理由这样做的情况下优先考虑定义的行为,因此只认为有必要确保在实现可能有理由偏离的情况下规则定义常见行为他们。 (2认同)