【问题标题】:Why special rules for `for` statement scope?为什么对“for”语句范围有特殊规则?
【发布时间】:2014-10-15 08:11:44
【问题描述】:

我最近遇到了这个问题

for(int i=0,n=v.size(); i<n; i++) {
   ...
   P2d n = ...   <<<--- error here
}

编译器抱怨 n 局部变量已被定义,尽管左大括号看起来应该开始一个新的范围。

事实上,该标准对此有一个特殊的措辞,虽然使用 g++4.6.3 编译的代码很好,但它与更新的版本和其他编译器相冲突。

这个特殊规则背后的基本原理是什么(如果有的话)?

更清楚地说:标准说明这是不允许的,我对错误的技术原因没有任何疑问:我只是想知道为什么委员会决定使用特殊的额外规则,而不是在看到左大括号时创建另一个嵌套范围(就像在其他地方发生的那样)。

例如,为了使代码合法,您可以用 两个 大括号对而不是一个...

还请注意,for/while/if 之后的大括号虽然被认为是一种很好的做法,但 不是强制性的,也不是语法的一部分,但仍然存在包含循环变量的范围(因此使用函数定义作为另一个本地变量的范围是函数体的例子是不相关的:函数体不是语句,大括号是强制性的)。

在 C++ 语法中,for 的主体只是一个语句;但是,如果该语句恰好是一个花括号组,那么它会在 for/while/if 中得到特殊处理(当您在语言的其他地方使用花括号组作为语句时不会发生这种情况)。

在语言中添加这种额外的复杂性的原因是什么?显然不需要,只是将大括号视为另一个内部范围似乎(对我来说)更简单。

是否存在这种更简单、更常规的方法不起作用的用例?

请注意,我不是在征求意见。要么你知道委员会为什么做出这个决定(在标准中还需要一个非常详尽的措辞,而不是仅仅将正文作为常规语句,并在用作语句时定期处理大括号括起来的块),或者你不知道。

编辑

语法的“单一范围”视图对我来说是不自然的,但对于 for 语句在技术上是可行的,可以通过向后的 goto 语句将其合理化为单个块,但很难在非常相似的情况下进行辩护if 语句的情况:

if (int x = whatever()) {
    int x = 3; // Illegal
} else {
    int x = 4; // Illegal here too
}

但这是合法的

if (int x = whatever()) {
    int z = foo();
} else {
    int z = bar();
}

那么条件、then 部分和if 语句的else 部分是否相同范围?不,因为您可以声明两个 z 变量。它们是单独的范围吗?不,因为你不能声明x

我能看到的唯一合理化是 thenelse 部分确实是单独的范围,但是添加了(奇怪的)规则,即条件中声明的变量不能在范围中声明。为什么会出现这个奇怪的限制规则是我要问的。

【问题讨论】:

  • 你基本上是在问“为什么这里没有两个嵌套范围?”
  • 我很想知道实际阅读问题的人发布了多少答案(因为您清楚地承认您知道它不起作用,请注意相关标准内容等)。
  • @OliverCharlesworth:你已经有了一个范围(例如,你可以放置一个不带大括号的语句)。问题是为什么不通过创建新范围而不是添加特殊规则来正常处理大括号...
  • @DavidHeffernan:将正文视为语言中的任何其他语句有什么混乱(即,如果它是一个花括号组,则开始一个嵌套范围)?
  • 我添加了 C 标签。没有多少问题值得同时使用 C 和 C++ 标记,但在这里似乎是合适的。 @OliverCharlesworth 的“因为 C 这么说”的回答自然会引出 C 为什么这么说的问题。

标签: c++


【解决方案1】:
int i = 0;
for (MyObject o1; i<10; i++) {
   MyObject o2;
}

可以从最近的编译器的角度翻译成:

int i = 0;
{
    MyObject o1;
    Label0:
    MyObject o2; //o2 will be destroyed and reconstructed 10 times, while being with the same scope as o1
    i++;
    if (i < 10)
        goto Label0;
}

这是你最后最后一个问号的答案,他们没有添加复杂的东西,只是在同一个范围内使用goto标记,而不是goto超出范围再进入。我看不出为什么它更好的明确原因。 (虽然它会与旧代码不兼容)

【讨论】:

  • +1:是的。在对象定义合法之前,我不知道向后跳。我删除了该示例,因为它确实无关紧要(可以看作是带有 goto 的单个范围)。为什么不简单地说主体是一个语句(如果你使用大括号,可能是一个嵌套范围)是一个仍有待回答的问题 IMO。
  • @6502 那么这个答案现在无关紧要了吗? (不管删不删)
  • 我觉得还可以。它只是表明单范围视图并非不可能(在我看来,它只是更复杂)。
  • @6502 好的,如果我能找到一些他们为什么改变它的来源,我会在这篇文章的末尾进行编辑。
【解决方案2】:

for 循环的语义并不特殊! if (bool b = foo()) { } 的工作原理相同。奇怪的是,它本身就是一个{ } 块。如果不引入新的作用域,那将毫无用处。因此,明显的不一致是由于对例外情况的错误概括。

[编辑] 另一种观点是考虑一个假设的、可选的关键字:

// Not a _conditional_ statement theoretically, but grammatically identical
always()
{
    Foo();
}

这统一了规则,你也不会期望这里有三个范围(内部、中间、外部)。

[edit 2](请不要将此作为回答的移动目标)

您想知道

中的生命周期和范围(两个不同的东西)
int i = 0;
for (MyObject o1; i<10; i++) {
   MyObject o2;
}

让我们概括一下:

MyObject o2; // Outer scope
int i = 0;
for (MyObject o1; i<o1.fooCount(); i++) {
   std::cout << o2.asString();
   MyObject o2;
}

显然,在所有迭代中,对o2.asString() 的调用指的是外部o2。这不像内部 o2 在循环迭代中幸存下来。当名称尚未在内部范围中定义时,名称查找不会使用外部范围中的名称 - 并且“尚未定义”是编译时的事情。内部o2的重复构造和销毁是运行时的事情。

【讨论】:

  • 我不确定我是否理解这个答案。 {} 引入范围是例外和奇怪的情况,if/for/while 是常见的常规情况?
  • 是的,您反对这样的解释吗? :) 毕竟,你多久会看到一个由 {} 单独引入的新区块?
  • 或许更重要的是,您是否经常看到一个裸体的{ } 用于引入本地范围以外的其他目的?相比之下,在 if (Bar) { Foo(); } 中看到多余的 {} 是很常见的。
  • 不太正确,“奇数”是另一个块内的任何块,无论它是孤独的还是由iffor 或其他什么引入的。
  • @MSalters:当退出包含它的范围时,INNER o2 被销毁。但它与包含 o1 的范围相同,但 o1 没有被破坏。流程是否退出该范围?为什么在for 之后对大括号的所有这些特殊处理都被认为很重要?这就是问题。有哪些用例可以证明这种增加的逻辑复杂性是合理的?
【解决方案3】:

这样看:
一对大括号允许您隐藏在封闭一对大括号(或全局)内可见的变量:

void foo(int n)
{
    // the containing block
    for (int i = 0; i < n; ++i)
    {
        int n = 5;  // allowed: n is visible inside the containing { }
        int i = 5;  // not allowed: i is NOT visible inside the containing { }
    }
}

如果你这样想,你会发现这里没有特殊的规则。

【讨论】:

    【解决方案4】:

    方括号 ({}) 将一段代码分隔为一个块。此块中的所有内容都在它自己的本地范围内:

    int main(int argc, char** argv)
    {
       int a  = 5;
       std::cout<<a<<std::endl      // 5
       {
           int a = 10;
           std::cout<<a<<std::endl  //10
       }
      std::cout<<a<<std::endl       // 5
    }
    

    但是等等,那段代码里还有别的东西……

    int main(int argc, char** argv)
    {
    }
    

    这类似于for循环的结构:

    for (int i = 0 ; i < 5; i++)
    {
    }
    

    函数定义在{...}块之外也有代码!
    在这种情况下,argcargv 被定义,并且它们在函数范围内是本地的,就像上面 for 循环中 i 的定义一样。

    其实你可以将语法概括为:

    definition { expression }
    

    以上所有内容均在范围内。
    在这种情况下,“原始”括号 ({}) 形成相同的结构,但定义语句为空。

    编辑: 回答您的编辑,在:

    int i = 0;
    for (MyObject o1; i<10; i++) {
       MyObject o2;
    }
    

    o2 的构造函数在每个循环中循环,而 o1 的构造函数则没有。

    for 循环行为如下(其中 XXX 是当前正在执行的块:

    1. 初始化
      for(XXX; ; ){ }
    2. 测试循环exp
      for( ;XXX; ){ }
    3. 执行块
      for( ; ; ){XXX}
    4. 最终操作
      for( ; ;XXX){ }
    5. 回到 2。

    【讨论】:

    • 问题是for (int i){int i} 变成了同一个作用域,就像最近的编译器版本中的int i; int i; 而不是以前的{int i;{int i;}}。 (编译时没有错误)
    • 查看我的答案,我刚刚发布了它。他们改变了for 的工作方式。 (我在那里展示了他们将其更改为那里的内容)
    • 因为我和你说你没看够:),这个问题是关于这个的。
    • 好的,所以我没有讨论为什么它在以前的编译器版本中编译正确?
    • 不,问题是为什么要添加这样的规则。最好读一下你这个懒惰的混蛋。 :D
    【解决方案5】:

    由于 标签,我将从这个角度回答。这是一个例子:

    #include <stdio.h>
    
    int main(void) {
        int a[] = {1, 2, 3, 4, 5, 6, 7, 8};
    
        for (int i = 0, n = 8; i < n; i++) {
            int n = 100;
            printf("%d %d\n", n, a[i]);
        }
    
        return 0;
    }
    

    它编译没有问题,看到它在ideone(C99 严格模式,4.8.1)下工作。

    C 标准明确将两个范围视为独立的,N1570 6.8.5/p5(强调我的):

    迭代语句是一个块,其范围是一个严格的子集 其封闭块的范围。循环体也是一个块,其 范围是迭代语句范围的严格子集

    有一个警告,但只有-Wshadow 选项,正如预期的那样:

    $ gcc -std=c99 -pedantic -Wall -Wextra -Wshadow check.c
    check.c: In function ‘main’:
    check.c:7: warning: declaration of ‘n’ shadows a previous local
    check.c:6: warning: shadowed declaration is here
    

    【讨论】:

    • 我没有添加C 标签。别人做了。
    【解决方案6】:

    循环控制变量(在本例中为in)被视为for 循环 的一部分。 而且由于它们已经在循环的初始化语句中声明,大多数在循环中重新定义它们的尝试(除了使用嵌套大括号重新定义)都会导致错误!

    【讨论】:

    • 据观察,如果您在第二组嵌套大括号中这样做,您可以重新定义循环中的变量。我不清楚 any 尝试重新定义 n 会导致错误,只有 some 尝试会导致错误。 (实际上,我怀疑您可以说“大多数”而不仅仅是“一些”。)
    【解决方案7】:

    我不能告诉你为什么for 循环只打开了一个范围,而不是因为大括号而打开了第二个范围。但我可以说当时给出的原因是改变单一范围的原因:局部性。采用这种非常标准的代码:

    void foo(int n) {
      int s=0;
      for (int i=0; i<n; ++i) {
        s += global[i];
      }
      // ... more code ...
      for (int i=0; i<n; ++i) {
        global[i]--;
      }
    }
    

    根据旧规则,这将是非法代码,在同一范围内定义 i 两次,该函数。 (在当时的 C 中,它甚至是非法的,因为你必须在块的开头声明变量。)

    这通常意味着你会在第二个循环中省略声明——如果第一个循环的代码被删除,就会遇到问题。不管你做了什么,你的变量都存在很长时间,这和往常一样让你的代码推理变得更加困难。 (那是在每个人和他们的兄弟开始认为十行长函数之前。)在此处的变量声明之前将for 更改为开始自己的作用域使代码更易于维护。

    【讨论】:

    • 地域性很重要这一点是绝对清楚的。在过去,当 MS 编译器(尽管有标准化)没有遵循这条规则时,我的代码使用 #define for if(0)else for 技巧来获得一个本地范围来包装 for 变量。然而,这与问题完全无关。请注意,for/if/while 删除 位置的额外特殊措辞......我不明白为什么认为这是必要的。
    • 我看不出这是如何消除局部性的。我所看到的只是块的范围已更改为包含“for (a;b;c)”部分。
    • 正如您在编辑后的问题中看到的那样,包含语句的单个范围的想法可以用作for 规则的解释,但需要您根据goto 进行思考并且对于if 语句中的同一问题也根本无法辩护,显然您不能考虑单个范围(因为您可以两次声明相同的变量)但您不能使用单独范围的想法(因为您不能重新声明x)。该规则强制在 body 语句内部定义外部定义(如果它是一个块),禁止使用相同名称的本地声明。
    • 没错,if 的范围有点奇怪。
    【解决方案8】:

    你的问题是for的定义部分被认为在for的范围内。

             // V one definition
    for(int i=0,n=v.size(); i<n; i++) {
       ...
        // V second definition
       P2d n = ...   <<<--- error here
    }
    

    【讨论】:

    • 很明显有错误。标准中的解释也有点复杂但合理。不清楚的是为什么他们不正常处理大括号而是创建一个新的内部范围......换句话说,为什么有一个特殊的规则。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-12
    • 1970-01-01
    • 2021-11-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多