切换布尔条件和标志






4.71/5 (32投票s)
提出了一种处理多个条件或标志的类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_x
和flags_x
,其中x
是从1到8的整数。switch_flags_x
宏开始switch flags块,并将条件作为参数,类似于switch
语句本身的使用方式。然后,flags_x
宏与case
标签结合使用来表示真值。它们接受令牌T
、F
或X
作为参数,分别表示true
、false
或任意。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;
}
性能
该库的性能是一个关键考虑因素。我不想让它比传统方法花费更多的时间或内存。实现方案通过利用编译器尽可能优化常量来很好地实现这一点。由于它完全由宏组成,因此不会增加二进制文件的大小。唯一需要考虑的性能权衡是该库不支持条件求值(请参阅上面的注意事项)。
后缀问题
总结
我创建这个库是为了解决我当时编写的一段非常难看的代码,并希望其他人也能使用它来美化自己的代码。如果您使用此代码,请告知我,并告诉我您认为任何有用的改进。一些可能的改进包括宏的变长版本、检测和自动消除重叠的case以及提高参数数量的限制。
历史
- 2011年1月3日:首次发布