有效,但在转换情况下毫无价值的语法?


207

通过一点错字,我偶然发现了这个构造:

int main(void) {
    char foo = 'c';

    switch(foo)
    {
        printf("Cant Touch This\n");   // This line is Unreachable

        case 'a': printf("A\n"); break;
        case 'b': printf("B\n"); break;
        case 'c': printf("C\n"); break;
        case 'd': printf("D\n"); break;
    }

    return 0;
}

语句printf顶部的似乎switch是有效的,但也完全无法访问。

我得到了干净的编译,甚至没有关于无法到达代码的警告,但这似乎毫无意义。

编译器是否应将其标记为无法访问的代码?
这有什么用吗?


51
GCC为此有一个特殊标志。是-Wswitch-unreachable
Eli Sadoff

57
“这有什么用吗?” 好吧,您可以goto进出本来无法到达的部分,这对各种黑客来说可能都是有用的。
HolyBlackCat

12
@HolyBlackCat难道不是所有无法访问的代码都这样吗?
伊莱·萨多夫

28
@EliSadoff确实。我想这没有任何特殊用途。我打赌它被允许只是因为没有理由禁止它。毕竟,switch这只是goto带有多个标签的条件。它的主体或多或少有与使用goto标签填充的常规代码块相同的限制。
HolyBlackCat

16
值得指出的是@MooingDuck榜样是达夫设备(上一个变种en.wikipedia.org/wiki/Duff's_device
迈克尔·安德森

Answers:


226

也许不是最有用的,但并非完全没有价值。您可以使用它来声明switch作用域内可用的局部变量。

switch (foo)
{
    int i;
case 0:
    i = 0;
    //....
case 1:
    i = 1;
    //....
}

标准(N1579 6.8.4.2/7)具有以下示例:

实例在人工程序片段中

switch (expr)
{
    int i = 4;
    f(i);
case 0:
    i = 17;
    /* falls through into default code */
default:
    printf("%d\n", i);
}

该对象的标识符i具有自动存储持续时间(在该块内),但从未初始化,因此,如果控制表达式的值为非零值,则对该printf函数的调用将访问一个不确定的值。同样,f无法调用该函数。

PS BTW,该示例不是有效的C ++代码。在这种情况下(N4140 6.7/3,重点是我的):

除非变量具有标量类型,具有简单的默认构造函数和琐碎的析构函数的否则程序会从不具有自动存储持续时间的变量不在范围内的点跳转到其处于范围内的点,跳变90,这些类型之一的cv限定版本,或者前述类型之一的数组,并且在没有初始化程序的情况下声明为(8.5)。


90)从switch陈述的条件到案件标签的转移在这方面被认为是跳跃。

因此,替换int i = 4;int i;使其成为有效的C ++。


3
“ ...但从未初始化...”看起来像i已初始化为4,我还缺少什么?
yano

7
请注意,如果变量是static,它将被初始化为零,因此也有一个安全的用法。
Leushenko '17

23
@yano我们总是跳过i = 4;初始化,所以它永远不会发生。
AlexD

12
嗯,当然!...问题的全部重点... geez。强烈希望消除这种愚蠢
yano

1
真好!有时,我需要在中添加一个temp变量case,并且始终必须在每个变量中使用不同的名称case或在开关之外定义它。
SJuan76 '17

98

这有什么用吗?

是。如果您在第一个标签之前放置一个声明,而不是一条语句,那么这很有意义:

switch (a) {
  int i;
case 0:
  i = f(); g(); h(i);
  break;
case 1:
  i = g(); f(); h(i);
  break;
}

通常,块的声明和语句规则是共享的,因此允许该语句也允许语句的规则相同。


还值得一提的是,如果第一条语句是循环构造,则case标记可能会出现在循环主体中:

switch (i) {
  for (;;) {
    f();
  case 1:
    g();
  case 2:
    if (h()) break;
  }
}

如果有更易读的方式编写代码,请不要这样写,但这是完全有效的,并且可以f()调用。


@MatthieuM Duff的设备在循环中确实有大小写标签,但是以循环之前的大小写标签开头。

2
我不确定是否应该为有趣的示例投票,还是为在真实程序中编写代码而疯狂:)。祝贺您跳入深渊,并一整天返回。
Liviu T.

@ChemicalEngineer:如果代码是循环的一部分(如Duff的设备中的代码),{ /*code*/ switch(x) { } }看起来可能更干净,但这也是错误的
Ben Voigt

39

此功能有一个著名的用途,称为Duff的设备

int n = (count+3)/4;
switch (count % 4) {
  do {
    case 0: *to = *from++;
    case 3: *to = *from++;
    case 2: *to = *from++;
    case 1: *to = *from++;
  } while (--n > 0);
}

在这里,我们将指向的缓冲区复制到指向from的缓冲区to。我们复制count数据实例。

do{}while()语句在第一个case标签之前开始,并且case标签嵌入在中do{}while()

这将do{}while()循环结束时遇到的条件分支的数量减少了大约4倍(在此示例中;可以将常数调整为所需的任何值)。

现在,优化器有时可以为您执行此操作(尤其是在优化流/矢量化指令时),但是如果没有配置文件引导的优化,它们将无法知道您是否期望循环很大。

通常,变量声明可以在那里发生并在每种情况下都可以使用,但是在切换结束后就超出了范围。(请注意,所有初始化都将被跳过)

此外,非特定于开关的控制流可以带您进入开关块的该部分,如上所示,或使用goto


2
当然,如果没有在第一种情况下的声明,这仍然是可能的,因为顺序do {case 0:无关紧要,都可以将跳转目标放在第一种情况下*to = *from++;
Ben Voigt

1
@BenVoigt我认为将放置起来do {更具可读性。是的,争论达夫装置的可读性是愚蠢和毫无意义的,并且可能是发疯的一种简单方法。
基金莫妮卡的诉讼

1
@QPaysTaxes您应该在C中查看Simon Tatham的协程。或者可能不是。
乔纳斯·谢弗(JonasSchäfer)

@JonasSchäfer有趣的是,这基本上就是C ++ 20协程为您服务的方式。
Yakk-Adam Nevraumont

15

假设您在Linux上使用gcc,如果您使用的是4.4或更早版本,它将发出警告。

-Wunreachable-code选项在gcc 4.4及更高版本中已删除


亲身经历问题总是有帮助的!
16吨,

@JonathanLeffler:不幸的是,gcc警告通常会受到选择的特定优化过程的影响,这仍然是真实的,这会带来糟糕的用户体验。拥有一个干净的Debug版本并随后发布一个失败的Release版本确实很烦人:/
Matthieu

@MatthieuM .:在涉及语法分析而不是语义分析的情况下,这样的警告似乎非常容易检测到(例如,代码遵循两个分支都返回的“ if”),并且更容易抑制“烦恼”警告。另一方面,在某些情况下,在调试版本中没有发布版本时出现错误或警告是很有用的(如果没有其他问题,应该清理用于调试的hack)释放)。
超级猫

1
@MatthieuM .:如果需要进行大量的语义分析才能发现某个变量在代码中的某些位置始终为假,则只有在执行了这种分析后,才能发现以该变量为真为条件的代码是不可达的。另一方面,我会注意到这样的代码是不可到达的,与关于语法上无法到达的代码的警告有所不同,因为在某些项目配置中可能有各种条件,而在其他情况下则不可能,这是完全正常的。有时可能对程序员有帮助...
supercat

1
...了解为什么有些配置会生成比其他配置更大的代码(例如,因为编译器可能认为某种条件在一种配置中是不可能的,而在另一种配置中是不可能的),但这并不意味着可以在其中进行优化的代码有任何“错误”具有某些配置的时尚。
超级猫

11

不仅用于变量声明,而且还用于高级跳转。只有当您不倾向于使用意大利面条式代码时,您才能很好地利用它。

int main()
{
    int i = 1;
    switch(i)
    {
        nocase:
        printf("no case\n");

        case 0: printf("0\n"); break;
        case 1: printf("1\n"); goto nocase;
    }
    return 0;
}

版画

1
no case
0 /* Notice how "0" prints even though i = 1 */

应该注意的是,开关箱是最快的控制流程条款之一。因此,它对于程序员来说必须非常灵活,有时会涉及到此类情况。


又什么区别nocase:default:
i486

@ i486 i=4不触发时nocase
Sanchke Dellowar '17

@SanchkeDellowar就是我的意思。
njzk2

为什么要这样做,而不是简单地将案例1放在案例0之前并使用法线转换呢?
乔纳斯·谢弗

@JonasWielicki在此目标中,您可以做到这一点。但是这段代码只是可以做什么的展示。
Sanchke Dellowar '17

11

应该注意的是,对于switch语句中的代码或case *:该代码中的标签放置位置* 实际上没有结构上的限制。这使得像duff的设备这样的编程技巧成为可能,其一种可能的实现如下所示:

int n = ...;
int iterations = n/8;
switch(n%8) {
    while(iterations--) {
        sum += *ptr++;
        case 7: sum += *ptr++;
        case 6: sum += *ptr++;
        case 5: sum += *ptr++;
        case 4: sum += *ptr++;
        case 3: sum += *ptr++;
        case 2: sum += *ptr++;
        case 1: sum += *ptr++;
        case 0: ;
    }
}

您会发现,switch(n%8) {case 7:标签之间的代码绝对可以到达...


* 正如supercat在评论中指出的那样:自C99以来,包含VLA声明的声明范围内都不会出现a goto和标签(无论是否为case *:标签)。因此,说标签的位置没有结构上的限制是不正确的case *:。但是,duff的设备早于C99标准,无论如何它并不依赖于VLA。然而,由于这个原因,我感到不得不在第一句话中插入“虚拟”。


可变长度数组的添加导致施加了与它们相关的结构限制。
超级猫

@supercat有什么样的限制?
cmaster-恢复莫妮卡

1
既不是goto也不switch/case/default标签可以由任何可变地声明的对象或类型的范围内出现。这实际上意味着,如果一个块包含任何可变长度数组对象或类型的声明,则任何标签都必须在这些声明之前。标准中有一些令人困惑的术语,这表明在某些情况下,VLA声明的范围扩展到switch语句的整个范围。看到stackoverflow.com/questions/41752072/…关于我的问题。
超级猫

@supercat:您只是误解了这个词(我想这就是您删除问题的原因)。它对可以定义VLA的范围提出了要求。它没有扩展该范围,只是使某些VLA定义无效。
基思·汤普森

@KeithThompson:是的,我误会了它。脚注中现在时的怪异用法令人困惑,我认为可以更好地表述为禁令:“ switch语句的主体中包含VLA声明,则该语句中不应包含任何switch或case标记。该VLA声明的范围”。
超级猫

10

您得到了与生成警告所需的gcc选项 相关的答案-Wswitch-unreachable,此答案将在可用性 / 有价值性部分进行详细说明。

直接引用C11第6.8.4.2章,(重点是我的

switch (expr)
{
int i = 4;
f(i);
case 0:
i = 17;
/* falls through into default code */
default:
printf("%d\n", i);
}

该对象的标识符i存在并具有自动存储持续时间(在该块内),但从未初始化,因此,如果控制表达式的值为非零值,则对该printf 函数的调用将访问一个不确定的值。同样,f无法调用该函数。

这是非常不言自明的。您可以使用它来定义局部范围的变量,该变量仅在switch语句范围内可用。


9

可以用它实现一个“循环半”,尽管这可能不是最好的方法:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));

@RichardII是双关语还是什么?请解释。
Dancia'1

1
@Dancia他说的是,这显然不是执行此类操作的最佳方法,而“可能不会”是一种轻描淡写的说法。
基金莫妮卡的诉讼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.