繁体   English   中英

C ++中不必要的花括号

[英]Unnecessary curly braces in C++

今天为同事做代码审查时,我看到了一件奇怪的事情。 他用这样的花括号括住了他的新代码:

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

它有时很好,因为它为您提供了一个新的范围,您可以在其中更“干净地”声明新的(自动)变量。

C++中,这可能不是那么重要,因为您可以在任何地方引入新变量,但也许习惯来自C ,直到C99才能做到这一点。 :)

由于 C++ 具有析构函数,因此在范围退出时自动释放资源(文件、互斥体或其他任何东西)也很方便,这可以使事情变得更干净。 这意味着您可以在更短的时间内保留某些共享资源,而不是在方法开始时抓住它。

一种可能的目的是控制变量范围 并且由于具有自动存储的变量在超出范围时会被销毁,因此这也可以使析构函数比其他情况更早地被调用。

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

在我的生产代码中,我写了这样的东西:

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 ,同时可以通过使用额外的大括号来定义其范围。 这样可以确保即使额外的大括号外的代码可以由多个线程同时执行,大括号内的代码一次只能由一个线程执行。

正如其他人所指出的那样,一个新块引入了一个新范围,使人们能够使用自己的变量编写一些代码,这些变量不会破坏周围代码的命名空间,并且不会超过必要的时间使用资源。

但是,这样做还有另一个很好的理由。

它只是隔离实现特定(子)目的的代码块。 很少有单个语句达到我想要的计算效果; 通常需要几个。 将它们放在一个块中(带有评论)可以让我告诉读者(通常是我自己在以后的日期):

  • 这个块有一个连贯的概念目的
  • 这是所需的所有代码
  • 这是关于块的评论。

例如

{  // update the moving average
   i= (i+1) mod ARRAYSIZE;
   sum = sum - A[i];
   A[i] = new_value;
   sum = sum + new_value;
   average = sum / ARRAYSIZE ;  
}

你可能会争辩说我应该写一个函数来完成这一切。 如果我只做一次,写一个函数只会增加额外的语法和参数; 似乎没什么意义。 只需将其视为无参数的匿名函数。

如果幸运的话,您的编辑器将具有折叠/展开功能,甚至可以让您隐藏块。

我一直这样做。 很高兴知道我需要检查的代码的范围,甚至更好地知道如果该块不是我想要的,我不必查看任何行。

一个原因可能是在新花括号块中声明的任何变量的生命周期都被限制在这个块中。 想到的另一个原因是能够在最喜欢的编辑器中使用代码折叠。

这与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可能很重要。

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

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

也许开发人员不信任此编译器的寄存器分配,或者希望通过一次限制范围内自动变量的数量来显式控制堆栈帧大小。

这里isInit可能会在堆栈上:

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

如果您取出花括号,即使在可能被重用之后, isInit的空间也可能会保留在堆栈框架中:如果有许多具有类似本地化范围的自动变量,并且您的堆栈大小是有限的,这可能是一个问题.

同样,如果您的变量被分配给一个寄存器,超出范围应该提供一个强烈的提示,即寄存器现在可以重用。 您必须查看使用和不使用大括号生成的汇编程序,以确定这是否会产生真正的差异(并对其进行分析 - 或观察堆栈溢出 - 看看这种差异是否真的很重要)。

我认为其他人已经涵盖了范围,所以我会提到不必要的大括号也可能在开发过程中起到作用。 例如,假设您正在对现有函数进行优化。 对于程序员来说,切换优化或跟踪错误到特定的语句序列很简单——请参阅大括号之前的注释:

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

这种做法在某些情况下很有用,例如调试、嵌入式设备或个人代码。

我同意ruak 如果您想很好地解释 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。

最后,请注意我的代码示例中使用的变量的范围:

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

那么,为什么要使用“不必要的”花括号呢?

  • 出于“范围”目的(如上所述)
  • 以某种方式使代码更具可读性(很像使用#pragma或定义可以可视化的“部分”)
  • 因为你能。 就那么简单。

PS 这不是坏代码; 它是 100% 有效的。 所以,这是一个(不常见的)品味问题。

在编辑中查看代码后,我可以说不必要的括号可能(在原始编码人员视图中)100% 清楚在 if/then 期间会发生什么,即使现在只有一行,也可能是以后再多行,括号保证你不会出错。

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

如果以上内容是原创的,并且删除“额外”将导致:

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

然后,以后的修改可能如下所示:

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

这当然会引起问题,因为无论 if/then 是什么,现在总是会返回 isInit。

另一个使用示例是与 UI 相关的类,尤其是Qt

例如,您有一些复杂的 UI 和许多小部件,每个小部件都有自己的间距、布局等。与其将它们命名为space1, space2, spaceBetween, layout1, ...您可以避免使用非描述性名称对于仅存在于两三行代码中的变量。

好吧,有些人可能会说你应该将它拆分为方法,但是创建 40 个不可重用的方法看起来不太好 - 所以我决定在它们之前添加大括号和注释,所以它看起来像逻辑块。

例子:

// Start video button
{
   <Here goes the code >
}
// Stop video button
{
   <...>
}
// Status label
{
   <...>
}

我不能说这是最好的做法,但它对于遗留代码来说是一个很好的做法。

当很多人将自己的组件添加到 UI 并且某些方法变得非常庞大时,就会遇到这些问题,但是在已经搞砸的类中创建 40 个一次性使用方法是不切实际的。

对象超出范围时会自动销毁...

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM