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
46727 次浏览

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

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

有时这很好,因为它给了你一个新的视野,在那里你可以更“干净”。声明新的(自动的)变量。

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,同时,可以通过使用额外的大括号来定义它的作用域。这可以确保即使额外大括号外的代码可以由多个线程同时执行,大括号内的代码将由正好是一个线程一次执行。

我同意与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

当物体超出范围时,它们会自动被魔法摧毁。

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

然而,这样做还有一个很好的理由。

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

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

如。

{  // 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 (false) or if (0)
{
//experimental optimization
}

这种做法在调试、嵌入式设备或个人代码等特定环境中非常有用。

这与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来说很重要,当释放资源的需求不会自然地“下降”时;函数或控制结构的边界。

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

  • 为了“确定范围”(如上所述)
  • 使代码在某种程度上更具可读性(很像使用#pragma,或定义可以可视化的“部分”)
  • 因为你可以。就这么简单。

附注:这不是糟糕的代码;这是百分百有效的。所以,这是一个(不寻常的)品味问题。

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

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

如果以上是原来的,删除“extras”将导致:

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

然后,稍后的修改可能是这样的:

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

当然,这会导致一个问题,因为现在isInit总是会被返回,不管if/then。

其他人已经正确地讨论了范围、RAII等可能性,但既然您提到了嵌入式环境,还有一个潜在的原因:

也许开发人员不相信这个编译器的寄存器分配,或者希望通过限制作用域内自动变量的数量来显式控制堆栈帧的大小。

这里isInit可能在堆栈上:

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

如果去掉花括号,isInit的空间可能在堆栈框架中保留,即使它可能被重用:如果有许多具有类似局部作用域的自动变量,并且堆栈大小有限,这可能是一个问题。

类似地,如果你的变量被分配给了一个寄存器,超出作用域应该会强烈提示这个寄存器现在可以重用了。您必须查看使用和不使用大括号生成的汇编程序,以确定这是否有真正的区别(并对其进行分析—或观察堆栈溢出—以查看这种差异是否真的重要)。

用法的另一个例子是与ui相关的类,特别是Qt

例如,你有一些复杂的UI和很多小部件,每个小部件都有自己的间距、布局等。与其将它们命名为space1, space2, spaceBetween, layout1, ...,还不如将变量命名为仅存在于2 - 3行代码中的非描述性名称。

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

例子:

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

我不能说这是最佳实践,但对于遗留代码来说是一个很好的实践。

当很多人把他们自己的组件添加到UI中,一些方法变得非常庞大时,就会出现这些问题,但在类中创建40个一次性使用的方法已经搞砸了,这是不现实的。