在变量定义之前使用Goto - 它的值会发生什么?

18

这里有一个我想知道的问题。给定以下代码,我们能否确定它的输出?

void f() {
  int i = 0; 
  z: if(i == 1) goto x; else goto u; 
  int a; 
  x: if(a == 10) goto y; 
  u: a = 10; i = 1; goto z; 
  y: std::cout << "finished: " << a; 
}

根据C++标准,这段代码是否保证输出finished: 10?或者编译器是否可以在跳转到a之前占用存储了a的寄存器?


你是否基本上在询问goto语句是否是一个序列点? - Armen Tsirunyan
@Armen 如果 a 是类类型,那么在它之前跳转会调用它的析构函数。所以我想知道如果它是非类类型,那么在它之前跳转是否会使其值无效?所以跳转到 z,然后到 ua 的值是否仍为 10,或者可能是由局部变量或某些汇编操作在点 z 处使用的随机其他值? - Johannes Schaub - litb
C++0x标准更大,但写得更好。与此同时,我们陷入了法律纠纷而不是处理实际情况。我越来越确信上述代码是未定义的,即使合法,也应该被编译器拒绝。 - spraff
@Xaade 这有时是很有用的。尝试编写一个不使用异常的解析器,你可能会发现有很多凌乱但必要的跳转点。 - spraff
决策结构取代了goto。在某些情况下,可能会使goto看起来更易读,但它们从来不是必要的。 - Lee Louviere
5个回答

6

注意:请先阅读评论。Johannes通过一个标准引用就击败了我的整个论点。;-)


由于我没有C++标准,所以我必须从C标准进行推导。

令人惊讶的是(对我而言),第6.2.1章节 标识符的作用域 没有提到标识符在声明时的作用域(这是我猜测的)。在您的示例中,int a 具有块作用域,其“在关联块结束时终止”,这就是所有内容。第6.8.6.1章节 goto语句 表示,“goto语句不得从具有可变类型的标识符的作用域外跳转到该标识符的作用域内” - 但由于您的 goto 只在块内部(因此,在 int a 的作用域内)跳转,因此在ISO/IEC 9899:1999方面看来,这似乎是可以接受的。

我对此感到非常惊讶...

编辑#1:经过快速搜索后,我找到了C++0x最终草案。我认为相关声明如下(第6.7章 声明语句,突出显示我的部分):

可以转移到块中,但不能绕过具有初始化的声明。如果程序从未在自动存储期变量不在作用域的点跳转到它在作用域内的点,则该程序是非法的,除非变量具有标量类型、带有平凡默认构造函数和平凡析构函数的类类型、这些类型之一的cv限定版本或前述类型之一的数组且没有初始化。

我认为您的代码符合标准的要求。但请注意,它很丑陋。;-)

编辑#2:阅读您有关向后跳转可能破坏 int a 的评论后,我发现了以下内容(第6.6章节 跳转语句,突出显示我的部分):

跳出循环、块或向后超越已初始化的具有自动存储期的变量涉及到在传输点处在范围内但在传输点处不在范围内的具有自动存储期的对象的销毁。

首先,int a 没有“初始化”,如果我正确理解标准术语的话,它不是一个对象。


1
int虽然不是可变类型。关于a的作用域,C草案表示:“每个枚举常量的作用域始于其定义枚举器出现后。任何其他标识符的作用域始于其声明符完成后。”所以我确实从a的作用域外跳到了它的内部。 - Johannes Schaub - litb
@Johannes Schaub:这有效地使我的论点无效,因为我只能在短时间内找到了解“块作用域”这个术语的含义的模糊定义。就goto而言,我可以想象,实际上不经过声明并不那么重要。你引用的那段话很好地驳斥了这一点。我会让我的回答成为语言标准律师主义的反面例子。;-) - DevSolar
@Johannes Schaub:关于C语言的有趣之处在于:它不允许在语句后定义变量,对吧——或者在C99中可以吗? - René Richter
我只是关闭了你的C规范报价而已。 :) - Johannes Schaub - litb

6

6.7/3表示:

如果一个程序从局部变量不在作用域的点跳转到该变量在作用域的点,除非该变量具有POD类型(3.9)并且声明时没有初始化(8.5),否则它是非法的。

因此,这应该是可以的。

然后在6.6/2中:

退出作用域(无论如何实现),将按照它们的声明相反顺序调用所有已构造自动存储期限定符(3.7.2)的构造对象(命名对象或临时对象)的析构函数(12.4)。

我认为这意味着当你跳回到z时,a就死了,而你不能保证关于a的无初始化声明第二次执行时会产生什么效果。

请参见6.7/2:

每次执行其声明语句时,都会初始化具有自动存储期限定符(3.7.2)的变量。在块退出时(6.6),将销毁在块中声明的自动存储期变量。

所以对我来说,似乎没有保证你能得到10,尽管很难想象一个编译器不会这样做。


2
“似乎很难想象有一种编译器不会出现这种情况。” - 如果合法,我可以想象。比较i == 1可能通过将i复制到寄存器中,将1复制到寄存器中并进行比较来实现。同时,优化器可能已经决定将a存储在寄存器中。最后,当执行比较时,相同的寄存器可能被分配给a1,因为a不在作用域内。 - Steve Jessop

0

不允许放弃变量定义。这应该是一个错误。


它确实编译通过,并且至少在使用G++时打印出“finished: 10”,即使使用了“-Wall -Wextra -pedantic”选项。问题是标准对此有何规定。 - DevSolar

0

根据C++标准,输出finished: 10是否有保证?

我认为是的,必须保证!

为什么呢?因为a从声明到其作用域结束(函数结束)都存在,并且根据定义它只能被初始化一次,并从那时起保留其值直到销毁,即在函数结束时。


实际上,标准表示如果您在变量声明之前返回,则该变量将被破坏并可以再次初始化。 - Mark B

0

为什么不用汇编来确定这个问题呢?

结论:本地变量只会被声明一次;然而,每次都会执行所有赋值操作。

下面是证明。


我简化了这个问题:

main.cpp:

int f() {
  //asm(";start f()");

  //asm(";begin: ");
  begin:
  int i = 0;
  if (i == 0)
  {
    i++;
    goto begin;
  }

  //asm(";after goto");
  return i + 2;
}

int main()
{
  //asm(";before f()");
  int i = f();
  //asm(";after f()");
  return i;
}

然后我使用以下命令在Linux上将其编译为汇编代码(取消注释asm):

g++ -std=c++20 -I/usr/include/c++/11 -I/usr/include/x86_64-linux-gnu/c++/11 -S main.cpp

这个命令会生成main.s文件,它是main.cpp的汇编输出。以下是相关部分;我添加了一堆破折号以使其更明显:

#APP
# 2 "main.cpp" 1
-----------------------------------;start f()
# 0 "" 2
# 4 "main.cpp" 1
-----------------------------------;begin:
# 0 "" 2
#NO_APP
.L2:
    movl    $0, -4(%rbp)
    cmpl    $0, -4(%rbp)
    jne .L3
    addl    $1, -4(%rbp)
    jmp .L2
.L3:
#APP
# 13 "main.cpp" 1
-----------------------------------;after goto
# 0 "" 2
#NO_APP
    movl    -4(%rbp), %eax
    addl    $2, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   _Z1fv, .-_Z1fv
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
#APP
# 19 "main.cpp" 1
-----------------------------------;before f()
# 0 "" 2
#NO_APP
    call    _Z1fv
    movl    %eax, -4(%rbp)
#APP
# 21 "main.cpp" 1
-----------------------------------;after f()

然后,我使用以下命令编译main.cpp:

g++ -std=c++20 -I/usr/include/c++/11 -I/usr/include/x86_64-linux-gnu/c++/11 main.cpp -omain

编译成功。 然后我运行它:

./main

但是它一直卡在无限循环中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接