将对象转化为布尔值是一个非常常规的需求,在诸如 if (expr) {}
或 while (expr) {}
这样的语句中,都会要求对象能够隐式转化为布尔值。C++ 也提供了一系列的方式用于对象的隐式或显式类型转换。但是诸如转化为 int,都是一些比较简单的操作,唯独 bool 比较特殊。在 C++ 中,有太多的类型可以跟 bool 类型相互转换,比如 int, 指针,对象等等。这也带来了一个问题,哪种转换方式才是最好的,所带来的副作用的是最小的。这个问题也叫做安全布尔问题(Safe bool)。
隐式类型转换
讨论这一切之前,先简单了解一下 C++ 的隐式转换工作规则。针对于隐式转换,C++ 定义了一套隐式转换序列:
- 零个或一个标准转换序列
- 零个或一个用户自定义转换序列
- 零个或者一个标准转换序列
标准转化序列顾名思义,就是内置的一些隐式转换规则,比如 int 转为 bool,数组类型转化为指针。而用户自定义转换序列就是用户自己所定义的一系列隐式转换规则,对于第二标准转换序列,只有在实施了用户自定义转换序列之后才能运用。在官方文档中并没有给两个标准转换序列命名,所以分别按其使用的节点来称呼。
每次进行隐式转换的时候,编译器会先使用第一标准转换序列进行转换。如果无法正确进行转换,就会使用用户自定义转换序列进行转换,如果此时能够正确转换的话,就会使用第二标准转换序列进行最后的收尾工作。不过,如果是转换构造函数的话,就不会触发第二标准转换序列了。
举个例子:
1 | class A { |
在这个例子中,第一标准转换序列把 A*
转换为 const A*
,用户自定义转换序列把 const A*
转换为 int
,最后再经由第二转换序列,把 int
转换为 bool
。
弄清楚隐式转换的规则后,对后续所涉及到的隐式转换会有更好的理解。更为具体的例子,可以参考《彻底理解 c++ 的隐式类型转换》
安全布尔问题
在引入安全布尔问题之前,得先回答一个问题,自定义类型为什么需要布尔值转换。一个最为常见的场景就是,我们需要去判断资源是否存在。在传统的 C 语言的做法中,都是采用指针来引用资源。所以在任意一个地方,我们只需要去判断指针是否为空,即可判断资源是否存在。
1 | if (some_resource *p = get_resource()) { |
在 C++ 中,我们会把资源放置于对象内部,使用包括 RAII 的方法来管理资源,对于资源是否可用的判断就会复杂一些。针对于这一问题,一个最为简单的方案提供一个方法,通过该方法来判断内部资源是否可用。
1 | ResourcePtr ptr = get_resource(); |
这一方法最大的问题在于,不可能会存在一个约定俗成的命名用于确定资源是否可用,除了 is_valid
之外,还可以是 isValid, Valid, is_empty, empty
等等,调用者需要去记住这些方法名,才能正确的使用。这样的话,就会增加调用者的负担,而且还会增加代码的复杂度。此外,在模板编程的时候,这一方法会带来更为严峻的问题,它没法通用于所有的类型。
所以最为完美的方案就是,对象能够顺利的转换为布尔类型,调用者可以直接使用对象来进行资源的判断。这就带来我们所提及的安全布尔问题,所幸这一问题在 C++11 之后在语言层面上得到了完美的解决,但是可以了解一下,过去是如何解决这一问题的。
使用 operator bool
C++ 提供自定义类型转换的方法,所以最显而易见的方案就是利用 operator bool
来实现对象的布尔转换。
1 | class Testable { |
在实现好 operator bool
之后,我们就可以直接使用对象来进行判断了。
1 | Testable t; |
从某种程度上来说,作为语言本身所提供的方案,这本应该是完美无缺的。但是,这一方案也存在着一些问题。在上一节提到的隐式转换规则中说到,在调用了用户自定义类型转换之后,还会有一个标准转换序列用于进行一些收尾转换工作。正是因为这一规则,会带来几个可能违背我们想法的问题。
其一就是,布尔值可以很轻易的转换为 int 类型,所以就可能会写出下面这样的代码:
1 | class IntPtr { |
我们原本是想在判断了 ptr
是否为空之后,再通过 *
操作符获取到其中的值。但是由于忽略了 *
操作符,导致这段代码虽然可以通过编译,但是 val
的值只会是 0 和 1。显然这是不符合预期的。而且问题也很隐晦,很难发现。这里便是第二转换序列进行了一些收尾工作,将 bool
类型转换为 int
类型。
其次就是,像文件句柄或网络连接这样的资源之间是不可以互相比较的。但是由于定义了 operator bool
,使得这些资源在语言层面上能够进行比较,这显然也是不合理的。比如下面这样的代码:
1 | class IntPtr { |
ptr1
和 ptr2
作为一个指向 int
值的对象,它们之间的比较是没有任何意义的。但编译器并不会报错,而且继续跑下来,直到运行时才会出现问题。
使用 operator!
要在实现布尔值转换的基础上,解决上述两个问题,operator!
应该是一个比较不错的解决方案。具体可以看下面的代码实现:
对于布尔值而言,置否是一个很常见的操作。所以也可以利用其来实现布尔值的转换。这个方案可以规避
1 | class IntPtr { |
这一方法最大的问题在于不够直观,每次判断时至少都需要使用一个感叹号。而且并没有解决可比较的问题。
使用 operator void*
在上文提到过,C 语言使用指针来进行资源的可用性判断是一个很不错的方式。所以一个转换思路就是借助于这一现成方案,在各式指针中,void *
指针除了用于进行类型转换,基本就没有其他的操作了,所以可以将对象隐式转换为 void *
指针。
1 | class IntPtr { |
这个方案看起来是比较完美的,上文所提到的各类问题基本都可以完美的避过。所以标准库中,std::cout
就采用了这一种方案用于隐式转换。但是它同时也暗含着一个比较严重的问题,像下面语句是完全合法的。
1 | IntPtr ptr; |
根据墨菲定律,可能出问题的话,就一定会出问题。所以还是需要探讨一个更完美的方案。
使用内部 class
指针进行转换
使用内部 class
来进行布尔值的隐式转换也是一个不错的思路,具体的做法如下所示:
1 | class IntPtr { |
在类的内部创建一个不需要实现的内部类,而后提供一个方法,通过该方法进行类型转换。因为类没有实现,所以类型转换的目标是指向该内部类的指针。虽然这一方案能够实现布尔值转换并进行判断,但是这种方案一样存在着不容忽视的问题。
1 | IntPtr p1, p2; |
指向数据的指针是具备比较大小的能力的,这个能力在这一方案下反倒成了一个问题。所以该方案的可用性倒不如上面提到的一些方案。
解决方案
explicit
转换函数
从 C++11 开始,这一关键词被引入到转换函数上,用于在语言层面上禁止隐式转换和拷贝初始化。这一关键词的引入,可以在相关草案上看到比较完整的提议论述。
对于开发者而言,这个关键词所带来的好处不言而喻,与本文最相关的一点就是,布尔值的隐式转换问题从此有了一个完美的官方标准方案。
1 |
|
只需要添加一个关键词,即可将类的布尔值隐式转换牢牢限制在判断语句内。
旧版本的解决方案
如果是一个 C++11 之前的代码,如何实现一个比较完美的布尔值隐式转换方式呢?上文提到了四种解决方案,从直接使用语言提供的方案,到利用空指针和指向数据的指针。除了这些之外,还缺乏了一个指向函数的指针。
跟平时所使用的指针不同,指向函数的指针式不一样完全不一样的存在。在 C++ FAQ 中,作者有提及,指向成员函数的指针并不单纯的只是一个指针,其更像是一个特殊的数据结构。而且,在语言的标准中,函数和数据的存放位置并没有做定义,因此,指向函数的指针和指向数据的指针不能够互相转换。(1 2)。也正因如此,针对于指向数据的指针的那些操作,对于指向函数的指针来说,都是不可行的。
所以,可以利用指向成员函数的指针来完成这个布尔值的隐式转换操作。
1 | class Testable { |
我们需要定义一个成员函数指针 bool_type
,bool_type
的细节并不需要对外暴露,同时,还需要定义一个实现该成员函数指针类型的成员函数 this_type_does_not_support_comparisons
。之后就就需要再实现一个类型转换函数,将对象转换为 bool_type
。因为 bool_type
是一个指针,所以编译器能够自动对它进行布尔值的转换。
这一方案基本继承了转换为 void*
的方案,只不过,这里的 void*
被替换为了 bool_type
。同时,因为 bool_type
是一个成员函数指针,所以还能规避 void*
会被 delete
的问题。
不过该方案现在还并非完美,因为隐式转换的目标是一个指针,所以不同的 Testable
对象依旧是可以互相比较的。这个问题其实是一个非常严重的语义问题,所以还是需要再进一步完善。
1 | Testable test; |
对于这个问题的最优解法就是运算符重载,只需要让使用者在调用了 ==
后无法通过编译,就可以一劳永逸的解决这个问题。具体的代码如下所示:
1 | template <typename T> |
我们在声明 bool_type
的实现函数时,将其访问权限设置为了 private
,这样就无法通过外界进行访问。因此在重载 ==
的时候,可以通过调用一个无法访问的函数的方式来阻止编译。
因此,通过上述的操作,就可以在并不带来副作用的情况下,完美解决 Testable
对象的比较问题。
旧方案的推广方案
既然实现方案有了,接下来就是如何将其变为一个库,供各路调用者直接调用。一个简单的实现如下:
1 | class safe_bool_base { |
这里比较有意思的是,原文作者提供了两种调用方案。一种是通过继承 safe_bool
类,另一种是通过继承 safe_bool
模板类,这两者的区别就在于虚函数的调用开销。
汇总
这个问题有意思的地方在于,它是一个非常简单的问题,但是却有着非常多的解决方案。这些解决方案的优劣性也各不相同,但是都能够解决这个问题。这也是 C++ 的一个特点,它的灵活性非常强,但是也因此导致了很多的问题。这也是为什么 C++ 有很多的编码规范,以及很多的编码规范的原因。原文详见:Safe Bool Idiom。
- 本文标题:对象如何正确地转化为布尔值
- 本文作者:Jacksing
- 创建时间:2022-12-08 23:20:56
- 本文链接:https://wzzzx.github.io/C/the-safe-bool-idiom/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!