_Static_assert 在未使用的泛型选择中

_Static_assert in unused generic selection

看起来 typeof 运算符可能会被下一个 C 标准接受,我正在寻找是否有办法利用它来使用可移植的 ISO-C 创建宏可以获取传递给它的数组的长度,或者如果传递给它的指针则无法编译。通常泛型选择可用于在使用不需要的类型时通过将其从泛型关联列表中删除来强制编译器错误,但在这种情况下,我们需要一个默认关联来处理任何长度的数组,所以我试图为我们不想要的类型的泛型关联强制编译器错误。下面是宏的示例:

#define ARRAY_SIZE(X) _Generic(&(X), \
        typeof(&X[0]) *: sizeof(struct{_Static_assert(0, "Trying to get the array length of a pointer"); int _a;}), \
        default: (sizeof(X) / sizeof(X[0])) \
)

问题是即使选择的通用关联是默认关联,_Static_assert 也会出错。为了简单起见,由于手头的问题与 C23 中引入的任何内容无关,我们将制作一个测试程序,该程序可以明确地拒绝指向 int 的指针:

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_SIZE(X) _Generic(&(X), \
        int **: sizeof(struct{_Static_assert(0, "Trying to get the array length of a pointer"); int _a;}), \
        default: (sizeof(X) / sizeof(X[0])) \
)

int main(void) {

    int x[100] = {0};
    int *y = x;
    int (*z)[100] = {&x};

    printf("length of x: %zu\n", ARRAY_SIZE(x));
    printf("length of y: %zu\n", ARRAY_SIZE(y));
    printf("length of z: %zu\n", ARRAY_SIZE(z));
    printf("length of *z: %zu\n", ARRAY_SIZE(*z));
    return EXIT_SUCCESS;

}

-std=c11 构建上面的内容,我发现 _Static_assertARRAY_SIZE 的所有扩展上跳闸,而我预计只有使用 [=18] 的指针有问题=] 通用关联。

根据通用选择的 C11 标准的 6.5.1.1 p3,

None of the expressions from any other generic association of the generic selection is evaluated

这是 gcc 和 clang 中的一个错误,还是我在标准中遗漏了什么会导致在未使用的通用关联中对此 _Static_assert 进行编译时评估?

评估哪个泛型选择并不重要。

当作为 _Status_assert 一部分的表达式的值为 0 时,这被视为约束违规,编译器需要生成诊断。

您不能真正将 _Static_assert 与应该 return 值的表达式混合使用,例如 function-like 宏。您也许可以使用“穷人的静态断言”来解决这个问题,就像我们在 C11 之前使用的丑陋技巧之一:

#define POOR_STATIC_ASSERT(expr) (int[expr]){0}

#define CHECK(X) _Generic((&X), \
        int **: 0,\
        default: (sizeof(X) / sizeof(X[0])) \
)

#define ARRAY_SIZE(X) ( (void)POOR_STATIC_ASSERT(CHECK(X)), CHECK(X) )

此处调用逗号运算符以使宏 CHECK return 大小或零,以防类型有效或无效。然后再次调用同一个宏,从 function-like 宏 ARRAY_SIZE 中 return 编辑。这将导致来自 ISO C 编译器的一些神秘错误,例如“错误:ISO C 禁止 zero-size 数组”。


下一个问题是 _Generic 中的 &(X) 决不能保证归结为 int**,因此此宏不安全或不可靠。不过,关于数组大小,我们可以使用一个技巧。指向无大小(不完整类型)数组的指针与相同元素类型的每个数组兼容,无论其大小如何。宏可以重写为:

#define POOR_STATIC_ASSERT(expr) (int[expr]){0}

#define CHECK(X) _Generic((&X),              \
        int (*)[]: sizeof(X) / sizeof(X[0]), \
        default: 0)

#define ARRAY_SIZE(X) ( (void)POOR_STATIC_ASSERT(CHECK(X)), CHECK(X) )

这将适用于任何 int 数组,无论大小如何,但对其他一切都无效。

利用 Lundin 的回答中的一些建议,我提出了以下简化问题的解决方案:

#define STATIC_ASSERT_EXPRESSION(X, ERROR_MESSAGE) (sizeof(struct {_Static_assert((X), ERROR_MESSAGE); int _a;}))

#define NO_POINTERS(X) _Generic(&(X), \
    int (*)[]: 1, \
    default: 0 \
)

#define ARRAY_SIZE(X) ( (void)STATIC_ASSERT_EXPRESSION(NO_POINTERS(X), "Cannot retrieve the number of array elements from a pointer"), (sizeof(X) / sizeof(X[0])) )

为了使实际的 use-case 成为使用 typeof 的泛型类型,这应该会出现在 C23 标准中,请使用以下代码替换 NO_POINTERS 宏:

#define NO_POINTERS(X) _Generic(&(X), \
    typeof(*X) (*)[]: 1, \
    default: 0 \
)

通过将 _Static_assert 移到通用选择之外,它只会根据选择中实际 returns 的值进行评估,因此它不会因为存在于未使用的选择。此外,还从通用选择中删除了元素数量计算,以便可以在静态断言中安全地使用通用选择的表达式,即使您的数组是 Variable-Length 数组,需要在 run-time.

静态断言本身放置在一个匿名结构中,我们采用 sizeof 使其成为表达式的一部分。然后像 Lundin 的示例一样,我们使用逗号运算符计算该表达式,然后抛出并使用数组大小​​计算的结果。

有了这个,我们在获取静态数组和 VLA 中的元素数量时拒绝了指针,而且在尝试传入指针时我们得到了一条很好的编译器错误消息。