【问题标题】:switch case: case label does not reduce to an integer constant切换案例:案例标签不会减少为整数常量
【发布时间】:2019-04-08 19:06:43
【问题描述】:

我完全了解switch 语句背后的机制以及为什么需要整数常量。我不明白为什么以下case 标签不被视为整数常量。之后怎么样了?不存在的变量?有人可以分类吗? C 编译器真的有必要这么笨吗?

struct my_struct {
    const int my_int;
};

switch (4) {

case ((struct my_struct) { 4 }).my_int:

    printf("Hey there!\n");
    break;

}

当然……

error: case label does not reduce to an integer constant
  case ((struct my_struct) { 4 }).my_int:

编辑回答尤金的评论:

你是什么真正的用例?如果是整数常量,为什么要这么复杂?

我试图找到一个巧妙的技巧来在两个字符串之间切换,使用 union 而不是 struct,如下例所示:

#include <stdio.h>

union my_union {
    char my_char[sizeof(int)];
    int my_int;
};

void clever_switch (const char * const my_input) {

    switch (((union my_union *) my_input)->my_int) {

    case ((union my_union) { "hi" }).my_int:

        printf("You said hi!\n");
        break;

    case ((union my_union) { "no" }).my_int:

        printf("Why not?\n");
        break;

    }

}

int main (int argc, char *argv[]) {

    char my_string[sizeof(int)] = "hi";

    clever_switch(my_string);

    return 0;

}

…当然不能编译。

在我的机器上((union my_union) { "hi" }).my_int26984((union my_union) { "no" }).my_int28526。但是我不能自己写这些数字,因为它们取决于机器的字节序(显然我的机器是小端序的)。但是编译器知道后者,并且在编译期间确切地知道((union my_union) { "no" }).my_int 将是什么数字。

烦人的是我可以 已经做到了,但只能使用非常晦涩(而且效率稍低)的语法。下面的例子编译得很好:

#include <stdio.h>

void clever_switch (const char * const my_input) {

    #define TWO_LETTERS_UINT(FIRST_LETTER, SECOND_LETTER) ((unsigned int) ((FIRST_LETTER) << 8) | (SECOND_LETTER))

    switch (TWO_LETTERS_UINT(my_input[0], my_input[1])) {

    case TWO_LETTERS_UINT('h', 'i'):

        printf("You said hi!\n");
        break;

    case TWO_LETTERS_UINT('n', 'o'):

        printf("Why not?\n");
        break;

    }

    #undef TWO_LETTERS_UINT

}

int main (int argc, char *argv[]) {

    clever_switch("hi"); /* "You said hi!" */
    clever_switch("no"); /* "Why not?" */

    return 0;

}

所以问题仍然存在:C 编译器(或本例中的 C 标准)真的需要这么笨吗?

【问题讨论】:

  • 你的真正用例是什么?如果是整数常量,为什么要这么复杂?
  • 在大多数情况下,C 标准不会让编译器在编译期间处理对象。任何编译时评估只需要使用简单的值。与简单值相比,根据定义,对象是一个存储区域,其中字节表示一个值。 C 被设计成,或者至少最初是被设计成便携的和小型的。一个特性是交叉编译:编译器可以设计为在一种架构上运行,并编译程序以在另一种架构上执行。 C 标准对此有各种规定,例如单独的字符集……
  • … 用于编译和执行。所以考虑一个 C 编译器,它必须允许你把东西放在一个结构中然后提取它们,它必须模拟它们如何进入目标系统上的结构,而不是在本机系统上。只需将int 放入并取出,这似乎很简单。但是 C 表达式可能会变得复杂得多。您可以将int 放入,然后取出一个字节,使用转换为char *。我们不会要求 C 编译器来模拟它。当然,它可以做一些事情以使其适用于像您这样的简单 int 操作,但是 C 标准......
  • ... 在解释必须以这种方式支持哪些特定事物时会遇到一些麻烦。不支持这种事情更简单。它就足够了——没有这个特性,C 已经取得了巨大的成功。即使你不摆弄字节,考虑一下如果你定义一个巨大的数组,用一些值初始化,然后取出其中一个用作“常量”会发生什么。现在您正在强制编译器在编译时处理大量数据。

标签: c switch-statement


【解决方案1】:

虽然表达式((struct my_struct) { 4 }).my_int 在运行时确实被评估为4,但它不是常数。 switch-case 需要一个常量表达式。

我看到您已将 my_int 声明为 const。但这仅意味着它以后不能修改。这并不意味着表达式((struct my_struct) { 4 }).my_int 是常量。

如果你使用if-statement 而不是switch-case,你会没事的。

if (((struct my_struct) { 4 }).my_int == 4) {
    printf("Hey there!\n");
}

【讨论】:

  • 我猜 OP 声称这个表达式 可以 在编译时被评估。
  • 在 C99 及更高版本中,我相信 ((struct my_struct) { 4 }).my_int 确实符合标准使用该术语的意义上的常量表达式 - 但是而不是 整数常量表达式,这是 switch 案例所需要的。
  • @zwol,我刚刚尝试了带有 C99 编译选项的 OP 代码。我得到同样的错误。
  • @VHS 我只是在挑剔你的措辞。 OP 的代码在所有版本的 C 标准中都无效,但不是因为 ((struct my_struct) { 4 }).my_int 不是常量。它一个常量[表达式],但它不是一个整数常量表达式,它是常量表达式的一个受限子类。案例标签不仅需要是常量,而且是整数常量表达式。
【解决方案2】:

switch 语句中的case 标签需要一个整数常量表达式,其定义为:

整数常量表达式应具有整数类型,并且只能具有整数常量、枚举常量、字符常量、结果为整数常量的 sizeof 表达式、_Alignof 表达式和作为强制转换的直接操作数的浮点常量的操作数。整数常量表达式中的强制转换运算符只能将算术类型转换为整数类型,但作为 sizeof 或 _Alignof 运算符的操作数的一部分除外。

表达式((struct my_struct) { 4 }).my_int 不符合此定义的整数常量表达式,即使它是一个整数值表达式,其值可以在编译时确定。

【讨论】:

  • 你的回答有点重复。这有点像说“它不能做到,因为标准是这样说的,即使理论上它可以做到”。但我的问题是“真的需要它不能完成吗?”......
  • 你问为什么它不被认为是一个整数常量(表达式),我已经回答了。如果您改为问“为什么不能扩展 C 标准中整数常量表达式的定义以包含这种情况”,那么我的答案是,它可以,但它会使标准和编译器实现复杂化,并且大概这情况被认为不够重要,不足以证明增加的复杂性。
  • 好吧,够公平的!
【解决方案3】:

关于为什么 C 标准不允许编译器接受的问题

case ((struct my_struct) { 4 }).my_int:

...我们不能肯定地回答这个问题,因为这里没有人是 C 委员会的成员(据我所知,无论如何)而且这个设计决定是在 30 年前做出的,所以很有可能没有人谁在那里记得理由。

但我们可以这么说:

  1. 最初的 1989 C 标准有意遗漏了许多可以实现的特性,但只是在实现复杂性、编译时内存要求等方面付出了巨大的代价。例如,最初的基本原理因为标准中“常量表达式”和“整数常量表达式”的区别在于编译器不应该在编译时进行浮点运算。

    您要求的功能与实现大致一样困难

    static const int CONSTANT = 123;
    ...
    switch (x) { case CONSTANT: ... }
    

    不需要在 C 中工作(尽管它在 C++ 中)。

  2. 自 1989 年以来对 C 标准的新增内容相对较少,只是为了响应大量需求。特别是,据我所知,“实现此功能不再昂贵”并不是一个足够的理由。

这是我能给你的最好答案。

【讨论】:

  • 这看起来是最有可能的答案,谢谢。这让我越来越相信 C 语言是如此美丽,以至于它值得从头开始重新发明。
【解决方案4】:

这是一个最小公分母的事情。

C 标准说 ((struct my_struct) { 4 }).my_int 不满足对 case 标签施加的约束(即它们是整数常量表达式),因此兼容的 C 编译器不需要足够聪明就可以对其进行优化。

标准不禁止编译器优化它。 确实,优化它是 clang 所做的。

你的程序:

#include <stdio.h>
struct my_struct {
    const int my_int;
};

int main()
{
    switch (4) {

        case ((struct my_struct) { 4 }).my_int:

            printf("Hey there!\n");
            break;

    }
}

just works on clang,不过如果你用-pedantic编译你会得到一个警告。

在其他情况下,例如在区分 VLA 和常规数组时,整数常量表达式和其他整数表达式之间的区别也会影响其他构造,例如基于 switchgoto 的跳转,如果它们跳转到范围内,这些跳转将被禁止VLA。同样,只要进行了至少一次诊断,编译器就可以折叠它并允许此类跳转(clang 警告折叠,而不是跳转)。

如果您确实使用了这些结构并且您的编译器不会阻止您,那么您的程序将无法移植。

最后,整数的编译时常量在某些情况下也会影响类型。

我相信 Linux 内核使用类似于

的东西
#define IS_CEXPR(X) _Generic((1? (void *) ((!!(X))*0ll) : (int *) 0), int*: 1, void*: 0)

检测整数常量表达式(我听说,这是淘汰 VLA 任务的一部分)。

这基于 C 标准规则,即等于 0 的整数常量表达式转换为 (void*) 是一个空指针常量,而正则整数表达式转换为 (void*) 只是一个 void 指针,即使表达式的值已知为 0。确定三元类型的规则然后区分(void*) 表达式和空指针常量,如果X 是整数,则(1? (void *) ((!!(X))*0ll) : (int *) 0) 的类型为int *常量表达式,否则void *

大多数编译器可能不会让您如此轻松地绕过类型系统违规(尤其是在 _Generic 内部)。

【讨论】:

  • @madmurphy 不客气。 zwol 揭示了起源。我写了更多关于整数常量表达式如何与现在的 lang 交互的内容。有趣的是,最初关于什么需要或不需要在编译时评估的半任意决定如何变成一个如此交织到语言中的概念,以至于今天的主要编译器都没有完全正确地实现它:@987654322 @ 。我认为,您可能对重塑的必要性提出了很好的看法。
【解决方案5】:

嗯。为什么不可能看起来已经被彻底解释了...... ...那我就把这个留在这里吧。

case ((int)((struct my_struct) { 4 }).my_int):

clang 9、arch-linux、x86_64

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多