C++中不必要的花括号

211

今天为同事进行代码审查时,我看到了一个奇怪的东西。他用花括号包裹了他的新代码,就像这样:

Constructor::Constructor()
{
   // Existing code

   {
      // New code: do some new fancy stuff here
   }

   // Existing code
}

这样做会有什么结果(如果有的话)?为什么要这样做?这个习惯从哪里来?

这是嵌入式设备的环境。有很多用C++“包装”起来的遗留C代码。也有很多从C转到C++的开发人员。

在这部分代码中没有临界区。我只在这部分代码中见过它。没有进行重要的内存分配,只设置了一些标志位和进行了一些位操作。

被花括号包围的代码类似于:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
}

(不要在意代码,只需关注花括号... ;) )

花括号后面还有一些位操作、状态检查和基本信号传递。

我跟那个人谈过,他的动机是限制变量范围、命名冲突以及其他我无法理解的问题。

从我的角度来看,这似乎非常奇怪,我认为我们的代码中不应该有花括号。我看到所有答案中都有一些很好的例子说明为什么可以用花括号包含代码,但难道不应该将代码分成方法吗?

fsdf

104
当你问你的同事为什么这么做时,他的回答是什么? - Graham Borland
22
RAII模式常见。快速概述:http://c2.com/cgi/wiki?ResourceAcquisitionIsInitialization - Marcin
10
我讨厌不必要的花括号。 - jacknad
9
内部块中是否有任何声明? - Keith Thompson
17
也许他只是想在编辑器中轻松地“折叠”掉那个新的部分。 - wim
显示剩余8条评论
14个回答

314

有时候这样做很好,因为它给你一个新的作用域,在这个作用域中,你可以更“清晰”地声明新的(自动)变量。

C++中,这可能不那么重要,因为你可以在任何地方引入新的变量,但也许这个习惯来自于C,在那里你直到C99之前是不能这样做的。 :)

由于C++有析构函数,有些资源(例如文件、互斥锁等)离开作用域后会被自动释放,这非常方便,能够使代码更加简洁。这意味着你可以短暂地持有某些共享资源,而不是在方法开始时就获取它们。


43
+1 是指明确提到新变量和旧习惯。 - arne
48
+1 是因为使用块级作用域,可以尽快释放资源。 - Leo
10
把一个代码块用“if (0)”注释掉也很容易。 - vrdhn
21
@ossandcad,他们告诉你的方法“太短”了吗?这很难做到。90%的开发人员(包括我自己在内)都有相反的问题。 - Ben Lee
1
@displayName 这是不同的,因为您可以拥有不是自动的“新变量”,所以您需要手动为它们分配内存(例如使用“new”关键字)。 - Brhaka
显示剩余4条评论

176

一个可能的目的是为了控制变量作用域。由于具有自动储存的变量在超出作用域时会被销毁,因此这也可以使析构函数比它本来应该被调用的时间更早。


15
当然,实际上那个代码块应该被制作成一个独立的函数。 - BlueRaja - Danny Pflughoeft
9
历史注释:这是早期C语言中一种允许创建本地临时变量的技术。 - Thomas Matthews
14
我必须说,尽管我对我的答案感到满意,但这并不是最好的答案;更好的答案明确提到了RAII,因为它是你想要在特定点调用析构函数的主要原因。这似乎是“西部最快枪手”的情况:我发布得足够快,获得了足够早的赞同,从而获得了比一些更好的答案更快的赞同势头。不过我没有抱怨! :-) - ruakh
8
你过于简化了问题,"将它放在一个独立的函数中"并不是每个代码问题的解决方案。其中某个代码块可能与周围的代码紧密耦合,触及多个变量。使用 C 函数需要进行指针操作。此外,并非每个代码片段都是(或应该是)可重用的,有时该代码甚至本身就没有意义。我有时会在我的 for 语句周围放置代码块,以创建一个短暂的 int i; 在 C89 中。你肯定不是在建议每个 for 都应该在一个单独的函数中吧? - Anders Sjöqvist

102

额外的花括号用于定义在括号内声明的变量的作用域。这样做是为了在变量超出作用域时调用析构函数。在析构函数中,您可以释放互斥锁(或任何其他资源),以便其他线程能够获取它。

在我的生产代码中,我编写了以下内容:

void f()
{
   // Some code - MULTIPLE threads can execute this code at the same time

   {
       scoped_lock lock(mutex); // Critical section starts here

       // Critical section code
       // EXACTLY ONE thread can execute this code at a time

   } // The mutex is automatically released here

  // Other code  - MULTIPLE threads can execute this code at the same time
}

正如您所看到的,通过使用额外的大括号,您可以在函数中使用scoped_lock并定义其作用域。这样做可以确保即使在额外的大括号之外的代码可能被多个线程同时执行,而括号内的代码将由仅一个线程执行。


1
我认为更清晰的做法是:scoped_lock lock(mutex) //临界区代码 然后 lock.unlock()。 - sizzle
17
如果临界区代码抛出异常,会怎样呢?要么互斥锁将永远被锁定,要么代码不会像你说的那样“更干净”。 - Nawaz
4
@Nawaz:@szielenski的方法在发生异常时不会保留互斥锁。他还使用了一个scoped_lock,该锁将在发生异常时被销毁。我通常也喜欢为锁引入一个新的作用域,但在某些情况下,unlock非常有用。例如,在临界区内声明一个新的局部变量,然后稍后再使用它。(我知道我有点晚了,但是为了完整起见...) - Stephan

56

正如其他人所指出的那样,一个新的代码块会引入一个新的作用域,使得你可以编写一些具有自己变量的代码,不会破坏周围代码的命名空间,并且不会占用比必要更长的资源。

然而,还有另一个很好的理由。

它就是为了隔离实现特定(子)目的的一段代码。通常情况下,单个语句无法产生我想要的计算效果;通常需要几个语句。将它们放在一个代码块中(并附带注释)可以让我告诉读者(通常是以后的我):

  • 这个代码块有一个连贯的概念目的
  • 这里是所有需要的代码
  • 这里是关于这个代码块的注释。

例如:

{  // update the moving average
   i= (i+1) mod ARRAYSIZE;
   sum = sum - A[i];
   A[i] = new_value;
   sum = sum + new_value;
   average = sum / ARRAYSIZE ;  
}
你可能会反驳说我应该写一个函数来完成所有这些操作。但如果我只需要执行一次,编写函数只会增加额外的语法和参数,似乎没有必要。将其视为无需参数的匿名函数。
如果你很幸运,你的编辑器将拥有一个折叠/展开功能,甚至会让你隐藏代码块。
我经常这样做。知道我需要检查的代码范围是很愉快的事情,更好的是,如果那一块不是我想找的,我就不必查看任何行。

23

其中一个原因可能是,在新花括号块内声明的任何变量的生命周期仅限于此块。另一个值得考虑的原因是为了能够在喜爱的编辑器中使用代码折叠功能。


19
这与 if(或 while 等)块相同,只是没有 if。换句话说,你引入了一个作用域而不引入控制结构。
这种“显式作用域”通常在以下情况下有用:
  1. 避免名称冲突。
  2. 使用 using 进行作用域限定。
  3. 控制析构函数的调用时间。

示例 1:

{
    auto my_variable = ... ;
    // ...
}

// ...

{
    auto my_variable = ... ;
    // ...
}

如果my_variable恰好是两个在隔离环境中使用的不同变量的一个特别好的名称,那么显式作用域允许您避免发明一个新名称来避免名称冲突。
这也可以避免意外地在其预期作用域之外使用my_variable示例2:
namespace N1 { class A { }; }
namespace N2 { class A { }; }

void foo() {

    {
        using namespace N1;
        A a; // N1::A.
        // ...
    }

    {
        using namespace N2;
        A a; // N2::A.
        // ...
    }

}

实际情况下使用此功能的机会很少,可能表明代码需要重构,但如果您真正需要它,该机制是存在的。 示例3:
{
    MyRaiiClass guard1 = ...;

    // ...

    {
        MyRaiiClass guard2 = ...;
        // ...
    } // ~MyRaiiClass for guard2 called.

    // ...

} // ~MyRaiiClass for guard1 called.

当需要释放资源的需求不自然地“落”在函数或控制结构的边界之外时,这对于RAII可能非常重要。


15

其他人已经正确涵盖了作用域、RAII等可能性,但是由于您提到了嵌入式环境,还有一个潜在的原因:

也许开发者不信任这个编译器的寄存器分配,或者想通过限制同时在作用域中的自动变量数量来显式控制堆栈帧大小。

在这里,isInit 可能会放在堆栈上:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
}

如果去掉花括号,isInit空间可能会在堆栈帧中保留,即使在可能重新使用之后:如果有很多具有类似本地化作用域的自动变量,并且您的堆栈大小受限,则可能会出现问题。

同样,如果您的变量分配给寄存器,那么超出作用域应该提供强烈提示,表明该寄存器现在可以重新使用。您需要查看生成的汇编代码(有和没有花括号)才能确定这是否会产生真正的差异(并对其进行剖析 - 或者观察是否发生堆栈溢出 - 以查看这种差异是否真的重要)。


+1 好的观点,尽管我相当确定现代编译器可以在没有干预的情况下正确处理这个问题。(如果我没记错的话,至少对于非嵌入式编译器来说,从99年开始忽略了寄存器关键字,因为他们总是比你做得更好。) - Rup

15

在多线程编程中,当与关键段一起使用作用域锁时,这真的非常有用。在大括号(通常是第一个命令)中初始化的作用域锁将在块的结尾处超出作用域,因此其他线程将能够再次运行。


12

我认为其他人已经讲解了作用域的问题,所以我想提到不必要的大括号在开发过程中也可能有作用。例如,假设你正在对一个现有函数进行优化。对程序员来说,切换优化或跟踪错误到特定的语句序列非常简单 - 只需查看大括号前的注释:

// if (false) or if (0) 
{
   //experimental optimization  
}

这种做法在调试、嵌入式设备或个人代码等特定场景下很有用。


10

我同意 ruakh的观点。如果想要一个关于 C语言不同作用域层级的详细解释,请看这篇帖子:

C语言应用程序中的不同作用域层级

一般来说,在函数调用期间使用“块级作用域”有助于使用临时变量,而不必在整个函数调用的生命周期中跟踪该变量。此外,一些人使用它可以在多个位置方便地使用相同的变量名称,但这通常不是一个好主意。例如:

int unusedInt = 1;

int main(void) {
  int k;

  for(k = 0; k<10; k++) {
    int returnValue = myFunction(k);
    printf("returnValue (int) is: %d (k=%d)",returnValue,k);
  }

  for(k = 0; k<100; k++) {
    char returnValue = myCharacterFunction(k);
    printf("returnValue (char) is: %c  (k=%d)",returnValue,k);
  }

  return 0;
}
在这个例子中,我定义了两次returnValue,但由于它只是在块级作用域内,而不是函数作用域(例如,在int main(void)之后声明returnValue),我没有收到任何编译器错误,因为每个块对声明的returnValue的临时实例是不知情的。
一般来说,我不能说这是一个好主意(即,你可能不应该反复重用变量名称从块到块),但总的来说,这可以节省时间,并让你避免管理整个函数中returnValue的值。
最后,请注意我的代码示例中使用的变量的作用域。
int:  unusedInt:   File and global scope (if this were a static int, it would only be file scope)
int:  k:           Function scope
int:  returnValue: Block scope
char: returnValue: Block scope

很忙的问题,伙计。我从来没有100个赞过。这个问题有什么特别之处吗?好链接。C比C++更有价值。 - Wolfpack'08

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