C++ 编程技巧:基于位标志(Bit Flags)的枚举类型设计
在编程中我们经常会设计一些枚举类在简单的应用场景里枚举都是针对单一枚举值进行处理的但在某些场景里我们需要处理多种枚举值联合生效的情形这时普通的应对方法在 if 中使用大量的 AND 和 OR 运算符把各种枚举值的判值表达式串联起来这种处理方法冗长而笨拙实际上有很好的解决方法就是 基于位标志Bit Flags的枚举类型。1. 位标志Bit Flags其实大家对位标志Bit Flags并不是陌生最典型也是最为人所熟知的就是Linux 的文件属性了r (read): 4w (write): 2x (execute): 1r/w/x 就是文件属性的三种枚举值而 Linux 给它们的取值就是充分利用了位标志Bit Flags使得它们可以进行“叠加”且叠加出的数值不会有歧义w x 3r x 5r w 6r w x 7Linux 文件属性的这些取值不是随意的因为 r/w/x 是三种状态因此设计方案是用三位二进制数来表示让其中一位为 1 其它位都为 0 的值代表一种状态则三种状态就是“可叠加”的也就是r: 100w: 010x: 001这样三种基础状态任意两两或全部叠加出的值都是“唯一”值不会冲突也没有歧义且一个值8也可以唯一拆解成一种或二种或三种状态的叠加也不会存在第二种可能的组合。这个逻辑可以总结为假设有一个包含 n 个元素的集合我们可以用一个 n 位的二进制数来表示这个集合仅第 i 位为 1其他位为 0 时表示是第 i 个元素 即2^0, 2^1, 2^2, 2^3, ……这样设计出的取值可以“叠加”以判断一个值是否是多个元素的组合逻辑是第 i 位为 1 表示集合中包含第 i 个元素第 i 位为 0 表示集合中不包含第 i 个元素我们把基于这种规则设计数值称为“Bit Flags”基于“Bit Flags”有三种经典位运算可以快速进行一些逻辑判断1.1 用 | 组合多个值将多个 flag 进行按位或运算得到的值就代表这多个 flag 的“组合”。以 Linux 文件属性为例4 | 1 5即5 表示可读和可执行两种标志位的组合1.2 用 判断是否包含某个值将一个值多个 flag 的组合和一个 flag 进行按位与运算得到非 0 值true)就表示包含这个值组合开关包含这个 flag! 以 Linux 文件属性为例5 2 0即5 不包含 2 可写状态1.3 用 ^ 去掉某个值将一个值多个 flag 的组合和一个 flag 进行按位异或运算得到的值组合开关就是去除掉这个 flag 后的新值新组合开关。以 Linux 文件属性为例7 ^ 4 3即从 7 中“关闭”只读属性开关后得到的值新组合开关是 3也就是 r x 不再有 r )Tips: 位运算器 https://www.freecodeformat.com/zh-hans/bitwise-calculator2. 基于 Bit Flags 的枚举基于 Bit Flags 的上述特性将它们声明成整型枚举类然后使用位运算符对它们进行操作就可以直接实现上述三种逻辑就省去了冗长的逻辑判断这就是基于位标志Bit Flags设计枚举类型的主要动机我们来看一个实例。假设有如下枚举类型enumVisualAttribute{AttrBrushColor0x001,AttrBrushTexture0x002,AttrPenCapStyle0x004,AttrPenColor0x008,AttrPenJoinStyle0x010,AttrPenPattern0x020,AttrPenScalable0x040,AttrPenWidth0x080,AttrSymbolColor0x100,AttrTextColor0x200,AttrTextFontFamily0x400,AttrTextHeight0x800};如前面介绍的当看到枚举取值是0x10x20x4时你就应该立即意识到这是准备要使用 Bit Flags 了后面必定会有基于这些枚举值的“位运算”。 至于为什么使用十六进制来书写这些枚举值是因为使用十六进制表示 2 的 n 次方有规律可寻不需要计算既可知道下一个数值是什么。从上面整齐排列的数值不难发现它的规律2 的 n 次方数使用十六进制表示时有固定的规律最初是 1、2、4、8到 16 时要进位变成 10然后就变成 10204080再进位又变成 100200400800依次类推。当枚举值较多时使用十六进制表示的优势就会变得很明显。现在我们了枚举类型要怎么用呢首先既然枚举都是整型值是可以直接进行位运算的// 不安全只是 int 类型没有类型检查intattrsAttrPenColor|AttrPenWidth;但是这样没有枚举类型检查不够安全比如你可以使用任意一个 int 值而不是枚举中有的值去和一个枚举进行位运算// 没有枚举类型检查任意整型都可以和枚举进行位运算 int attrs AttrPenColor | 9为了解决这个问题Qt 针对 Bit Flags 型枚举提供了一些封装和支持可以帮助我们更方便地使用这种枚举具体就是Q_DECLARE_FLAGS和Q_DECLARE_OPERATORS_FOR_FLAGS两个宏。2.1 Q_DECLARE_FLAGS: 把枚举转变成 QFlags标识位集合我们对普通枚举类型VisualAttribute的改造只需一步使用Q_DECLARE_FLAGS声明一个新类型Q_DECLARE_FLAGS(VisualAttributes,VisualAttribute);上述宏展开就是这样一行代码typedefQFlagsVisualAttributeVisualAttributes;它声明了一个新类型QFlagsVisualAttribute然后把这个类型取名为VisualAttributes。QFlags 是一个模板类它提供了一种类型安全的方式来存储枚举值的组合叠加值也就是说声明了上述宏之后我们就可以这样写了VisualAttributes attrsAttrPenColor|AttrPenWidth;也就是说attrs就代表同时拥有AttrPenColor和AttrPenWidth两个枚举的转态值你甚至可以打印出它的具体值std::cout attrs std::endl; // 136且由于VisualAttributes的类型检查以下书写将无法编译通过因为// 编译错误9 不是合法的枚举置。VisualAttributes attrsAttrPenColor|9;2.2 Q_DECLARE_OPERATORS_FOR_FLAGS为 QFlags标识位集合添加位运算符函数不过上述定义 attrs 的代码是编译不过的编译器会报Clangd: No viable conversion from int to VisualAttributes (aka QFlagsVisualAttribute)因为类型不匹配系统里没有从 int 到 VisualAttributes 的自动类型转换此时还要使用第2个宏来为 VisualAttributes 添加位运算符函数以便它能直接参与到位运算中而不会报类型错误Q_DECLARE_OPERATORS_FOR_FLAGS(VisualAttributes);上述宏会为VisualAttributes添加operator|()、operator()、operator^()、operator~()及其赋值形式、|和^等多个运算符函数使得VisualAttributes可以丝滑参与到各种位运算中。以下是一些示例// 1. 组合多个属性VisualAttributes attrsAttrPenColor|AttrPenWidth;// 2. 判断是否包含某个属性if(attrsAttrPenColor){std::coutAttrPenColor is included!std::endl;}// 3. 追加一个属性attrs|AttrTextColor;// 4. 移除一个属性attrs~AttrPenWidth;至此关于基于 Bit Flags 的枚举的使用以及在 Qt 下的支持功能都介绍完毕了。下面是来自实际项目中更有说明性的一个示例。3. 真实项目案例在开源 Markdown 编辑器 VNote 中有一种叫原地预览In-Place Preview的功能它能在编辑窗口中某类 Markdown 文本块的下方就地渲染出文本的实际效果这对于图片、代码块、公式等使用特殊语法描述的内容非常有用用户可以更具原地渲染的结果来判断有没有写错代码或适时地调整代码。针对不同的内容VNote 设计了可以分别配置是否需要进行原地预览这个配置项是inplace_preview_sources:imagelink;codeblock;math默认配置是允许三种类型imagelink、codeblock、math一共只有这三种的代码进行原地预览。用户如果不需要某些类型的代码原地预览可以从配置中移除它们。与此同时在代码中程序要读取这里的配置并转换成枚举值。VNote 在这里的设计就是基于 Bit Flags 的。首先设计四个基础枚举类型然后如同前面示例中一样通过Q_DECLARE_FLAGS定义出这组枚举类的QFlags 集合类型InplacePreviewSources然后使用Q_DECLARE_OPERATORS_FOR_FLAGS为其添加位操作符函数// 定义枚举enumInplacePreviewSource{NoInplacePreview0,ImageLink0x1,CodeBlock0x2,Math0x4};// 定义枚举的 QFlags 集合类型Q_DECLARE_FLAGS(InplacePreviewSources,InplacePreviewSource);// 为 InplacePreviewSources 添加位操作符函数Q_DECLARE_OPERATORS_FOR_FLAGS(vte::MarkdownEditorConfig::InplacePreviewSources)// 声明一个QFlags 集合类型的变量它将用于存储从配置中读取出的多个枚举值的“组合”InplacePreviewSources m_inplacePreviewSources;将下来第一个要解决的问题就是如何把配置中启用的 In-Place Preview 类型读取到程序中这发生在下面的代码中代码从配置中读取到配置值imagelink;codeblock;math然后 split 成三个字符串再把它们转换成对应的枚举值然后关键的就是在 for 循环中叠加进行位或运算m_inplacePreviewSources | ...这一操作的实质是将配置中出现的各种类型“组合”成一个变量m_inplacePreviewSources虽然它是一个值但却能清除地表示具体配置了哪些类型。如果不是使用 Bit Flags这个变量可能就会是一个 Map 了key 是类型名value 是 true|false既然这里使用了 Bit Flags那么后续的逻辑判断也将基于位运算进行。下面使用m_inplacePreviewSources的一断代码也就是要根据配置m_inplacePreviewSources就代表着配置数据了进行相应的处理。其中典型的操作就是用m_inplacePreviewSources和 某一个枚举值进行位与运算用来判定配置中有没有这个类型