【问题标题】:When and how are VLAs evaluated in sizeof expressions?何时以及如何在 sizeof 表达式中评估 VLA?
【发布时间】:2020-11-11 05:33:23
【问题描述】:

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;
}

输出(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

【问题讨论】:

  • sizeof(*pu2) does not evaluate *pu2, contrary to the Standard specification 你的意思是,如果sizeof(*pu2) 被评估,你会期望foo() 被调用?
  • 我喜欢使用int i = 0; char a[rand()%2 + 1]; printf("%zu\n", sizeof a[i++]); printf("%d\n", i); 递增iint i = 0; char a[42]; printf("%zu\n", sizeof a[i++]); printf("%d\n", i); 不会增加i,因为sizeof 的参数中的i 未评估。 @chqrlie 这是否接近您所寻求的?
  • sizeof(*pu2) 不评估 *pu2,这与标准规范相反 也许它已被评估并且您得到未定义的行为,看起来它没有被评估。
  • @LanguageLawyer:鉴于pu2 被显式初始化为NULL,评估*pu2 应该有未定义的行为,这确实可能会被忽视,例如如果代码被省略,这是相当可能是因为 U 类型的大小可以在不查看 pu2 的情况下确定。
  • @KamilCuk:我不希望评估foo(),但取消引用空指针应该有可见的副作用,尽管标准没有要求。事实上,即使pu2volatile 限定的,指针也不会被clang 解引用。

标签: c language-lawyer c99 variable-length-array expression-evaluation


【解决方案1】:

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

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

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

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

int rows, cols;
...
T (*arr)[cols] = malloc( sizeof *arr * rows );

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

【讨论】:

  • 这只是众多案例之一,充分迂腐阅读标准的部分内容将被描述为未定义的行为行为,这些行为具有一个含义,否则可以通过阅读标准的其他部分和平台的文档。该标准的作者期望编译器编写者在他们没有理由不这样做的情况下会优先考虑定义的行为,因此只认为有必要确保在实现可能有理由偏离的情况下规则定义常见行为他们。
  • @supercat 说只有“可以评估大小表达式”就足够了。 “可能”部分甚至已经在port70.net/~nsz/c/c11/n1570.html#6.7.6.2p5
【解决方案2】:

每个可变修改类型都有一个大小,对于每个维度,它要么是该维度的倍数,要么独立于该维度。没有理由评估可变修改对象的大小应该需要评估不会影响对象大小的任何维度的值,但是一些编译器可能会评估这些维度的值,因为可变修改类型的原始规则暗示他们应该被评估。在不同的实现以不同方式处理构造的情况下,标准的作者倾向于避免让标准建议任何一种行为都更好。因此,该标准故意对涉及可变修改类型的极端情况模棱两可,以避免将任何现有实现的行为描述为“错误”或劣等。

【讨论】:

  • 因此,标准对涉及可变修改类型的极端情况故意模棱两可...我认为它应该更加模棱两可并声明 如果类型的操作数是可变长度数组类型,操作数可以被评估以确定类型的大小。这将使涉及逗号运算符 undefined 的极端情况变得更糟,例如 int n=1, a[n]; (void)sizeof *(printf("Gotcha!"), &amp;a);
  • @chqrlie:如果标准为表达式可以使用的不同方式定义术语,包括“已解决”[使用赋值运算符左侧的左值或&amp; 的右侧,或作为隐式数组衰减的结果],以及“大小”[通过 sizeof 完成的操作]。然后可以根据评估、解析和调整哪些部分来指定评估、解析和调整表达式的效果。
  • 这是否意味着标准引入了荒谬的要求,迫使编译器开发人员实现它,现在使用保持向后兼容性作为保留要求的参数? ..看起来像第 22 条渔获
猜你喜欢
  • 2016-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-15
  • 2013-11-20
  • 2012-02-01
  • 2019-02-18
  • 2014-06-21
相关资源
最近更新 更多