65.9K
CodeProject 正在变化。 阅读更多。
Home

切换布尔条件和标志

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (32投票s)

2011年1月3日

BSD

8分钟阅读

viewsIcon

72909

downloadIcon

329

提出了一种处理多个条件或标志的类switch语法技术和代码。

引言

如果你曾经编写或看过包含多个嵌套if语句的代码,你知道它有多难维护。在这篇文章中,我将介绍一种代码,在某些情况下可以消除深度嵌套的需要。嵌套的if语句被改为更像熟悉的、并且更易于维护的switch语句。

背景

当编写行为依赖于多个条件或标志的代码时,确定该行为的常规方法是尽可能有效地检查这些条件,以使代码不会变得过于复杂。当需要检查多个条件时,if语句的嵌套可能会非常深,并且难以维护。

在某些情况下,必须在嵌套的复杂性和匹配条件时执行的代码之间取得平衡。代码可以穿插在嵌套本身中,但是当使用复杂的嵌套时,执行的真实流程可能会变得难以跟踪。复制代码可以使流程更容易跟踪,但会以与代码复制相关的维护问题为代价。无论哪种情况,嵌套本身及其相关的括号都无助于使代码简洁。

// An example of complex nested if statements
if (foo > 1)
{
  if (bar < 12)
  {
    if (foo < bar+10)
    {
      // rare case code here looks more important than it is
    }
    else
    {
      // common case code
    }
  }
  else if (eof())
  {
    // same rare case code copied from above
  }
}  

简化此代码的一种技术是将条件的结果存储在if语句嵌套之上的布尔值中。然后,在if语句条件中使用这些布尔值。在这种情况下,条件只评估一次,这可能是一个重要的考虑因素。虽然这简化了条件,但并没有解决嵌套问题。另一种技术是在不需要时仔细消除花括号的使用。但是,当存在else子句时,这可能会导致代码逻辑不清晰。该技术通常不推荐,并且经常在编码标准中被明确禁止。

解决此问题特定版本的一种方法是switch语句。它简化了重复的else if块的结构。例如,这

if (a == 1)
  ;
else if (a == 2)
  ;
else if (a == 3)
  ;
else
  ;

可以变成这样

switch (a)
{
case 1:
  break;
case 2:
  break;
case 3:
  break;
default:
  break;
}

现在,a只求值一次,并且case被很好地分解,没有很多额外的语法噪音。switch语句的问题在于它们不能处理需要测试多个条件的情况。它们也只适用于特定类型的条件:相等性。

Switch Flags 库

为了解决其中一些问题,我编写了一个单头文件(switch_flags.h)形式的库,它允许switch语句更加灵活。Switch Flags库允许switch语句使用多个条件。它由两组宏组成,switch_flags_xflags_x,其中x是从1到8的整数。switch_flags_x宏开始switch flags块,并将条件作为参数,类似于switch语句本身的使用方式。然后,flags_x宏与case标签结合使用来表示真值。它们接受令牌TFX作为参数,分别表示truefalse或任意。flags_x宏中的每个参数都对应于控制switch_flags_x宏中的匹配条件。

一个简单示例

这个简单的例子演示了基本用法

#include <switch_flags.h>

switch_flags_2(a > b, c != d)
{
case flags_2(T,T):
  // only execute when "a > b" and "c != d"
  break;
case flags_2(F,X):
  // execute when "a > b" is false
  break;
}  

让我们逐行分析。

#include <switch_flags.h>

包含构成库的头文件。由于该库是头文件,因此不需要进行链接更改。只需将switch_flags.h复制到您的项目中,并在需要的地方#include 它。

switch_flags_2(a > b, c != d)

开始switch flags块并呈现条件。由于C预处理器的限制,对于不同数量的条件,宏有不同的版本,每个版本都有一个后缀,即参数的数量。移除此要求是该库的一个可能改进(请参阅下面的“工作原理”部分)。

每个条件的参数位置很重要。它们将需要与后续flags_x宏的参数位置匹配。条件将在此时求值,并且只求值一次。后续的flags_x宏将使用此处求值的结果进行测试。

{ 

打开括号以开始switch 块,就像普通switch 块开始一样。

case flags_2(T,T):
  // only execute when "a > b" and "c != d"
  break;

第一个case 标签和关联的块。flags_2宏呈现要检查的特定情况。在这里,它使用令牌T为两个参数,检查两个条件是否为真。

case flags_2(F,X):
  // execute when "a > b" is false
  break;

第二个case 语句和关联的块。flags_2宏再次呈现要检查的情况。但是,它现在使用F令牌检查第一个条件a > b是否为false ,并且使用X令牌根本不检查第二个条件c != d

} 

由于没有更多可能的case,我们关闭switch_flags_2块。

一个更复杂的例子

可以使用更复杂的用法,包括总共八个条件。例如,以下代码复制了上面提供的示例(请注意参数数量的变化以及宏后缀的相应变化)

#include <switch_flags.h>

switch_flags_4(foo > 1, bar < 12, foo < bar+10, eof())
{
case flags_4(T,T,F,X):
  // common code
  break;
case flags_4(T,T,T,X):
case flags_4(T,F,X,T):
  // rare case code
  break;
} 

default关键字也可以用于覆盖任何未明确指定的case,就像在switch 语句中一样。

注意事项

当然,使用这个库有一些注意事项。在使用嵌套if语句时,您可以控制条件本身的求值,以便它们只在特定条件下求值。当条件的求值本身可能导致性能问题或错误条件时,这可能很重要。然而,该库始终在每次都求值所有条件。没有条件求值。当然,条件求值在单个条件内是支持的,所以像ptr && ptr->foo这样的条件可以正常工作。

此外,由于库的实现方式使用了case标签,它具有case不能重叠的要求。也就是说,对于给定的条件,不可能有多个case标签满足特定的一组真值。对于常规的case标签,这通常不是问题,因为重叠的case很明显。使用这个库,X令牌可能会导致重叠,而这些重叠可能不那么明显。例如

case flags_3(T,X,T): // this case ...
case flags_3(T,T,X): // overlaps this case since they both cover the (T,T,T) case

一个更宽松的编译器可以通过允许重叠的case来解决这个问题。事实上,这似乎是C/C++中一个不幸且不必要的限制。

工作原理

该库完全由宏组成。switch_flags_x宏只是获取条件并计算一个整数值,其中每个位对应于一个条件。如果您不熟悉,这是一种在单个变量中存储多个标志的非常常见的技术。整数中的每个位根据其对应的条件是开还是关,取决于条件是真还是假。在C/C++中,允许switch语句求值其表达式。该库在此中使用此功能来在运行时求值条件并生成一个整数值,该值表示所有条件的组合状态。

然而,Case 语句不同,因为它们不允许在运行时对其操作数进行求值。它们必须是编译时求值出的常量。但是,它们可以通过简单地添加另一个case来包含多个常量在同一个块中。这就是库处理X令牌的方式,它使匹配的整数值的数量翻倍。每次在flags_x宏中出现X令牌时,宏必须“拆分”并确定swtich_flags_x宏创建的两个值集。然后会生成一个新的case标签。

这是简单的(两个参数)case在预处理后展开的样子

switch (((a > b) ? 1 : 0) | (((c != d) ? 1 : 0) << 1)) 
{ 
case ((((0<<1)|1)<<1)|1): 
  break; 
case ((((0<<1)|0)<<1)|0):
case ((((0<<1)|1)<<1)|0):
  break;
} 

稍微简化一下,上面的内容可以简化为

switch ((a > b ? 1 : 0) | ((c != d ? 1 : 0) << 1)) 
{ 
case 3: 
  break; 
case 0:
case 1:
  break;
} 

性能

该库的性能是一个关键考虑因素。我不想让它比传统方法花费更多的时间或内存。实现方案通过利用编译器尽可能优化常量来很好地实现这一点。由于它完全由宏组成,因此不会增加二进制文件的大小。唯一需要考虑的性能权衡是该库不支持条件求值(请参阅上面的注意事项)。

后缀问题

该库要求用户通过宏本身的名称上的后缀来冗余地指定参数的数量。这个问题的一个可能解决方案是使用C99中指定的变长宏。然而,这将把库的用户限制在那些拥有符合要求的预处理器的人。目前该库几乎可以与任何不错的C预处理器一起使用,这使得该库适用于非常广泛的用户。在库中添加变长形式的宏将是该库的一个可能改进。

总结

我创建这个库是为了解决我当时编写的一段非常难看的代码,并希望其他人也能使用它来美化自己的代码。如果您使用此代码,请告知我,并告诉我您认为任何有用的改进。一些可能的改进包括宏的变长版本、检测和自动消除重叠的case以及提高参数数量的限制。

历史

  • 2011年1月3日:首次发布
© . All rights reserved.