学习如何将一个对象转换成 bool
对象的同时不引入任何不利的影响
本篇文章主要是对下面这篇英文博客的翻译,并且 补充 了一些额外的注释
https://www.artima.com/cppsource/safebool.html
在 C++ 中,有很多方式可以为一个类提供布尔检测, 而提供这种支持是为了让类使用起来变得更加直观,支持通用编程,或者二者兼顾。我们将研究四种主流的方式来实现布尔检测这个功能。并且提供一种新的解决方案,该方案没有其他四种潜在的陷阱和危险。
目标
一些类型,类似于指针,允许我们以布尔的形式判断它们的有效性,算术,枚举,指针或指向成员类型的指针的右值可以转换为布尔值。零值,空指针值或空成员指针值将转换为 false
;任何其他值将转换为true
。
我们经常用这一特性来进行逻辑的处理,例如我们在获取资源的时候
if (some_type* p=get_some_type()) {
// p is valid, use it
}
else {
// p is not valid, take proper action
}
当然这种用法不仅仅对于内置类型有用,任何具有明确含义的类型如果使用这种布尔转换也可以大大提高代码的可读性 替代方式是使用该类型里的成员函数进行判断,例如,考虑判断智能指针(不包含隐式转换功能)的有效性:
smart_ptr<some_type> p(get_some_type());
if (p.is_valid()) {
// p is valid, use it
}
else {
// p is not valid, take proper action
}
除了更冗长之外,此版本与上一个demo里不同之处在于,对象 p
必须在其使用范围之外申明,从维护的角度来看,因为不经意间扩大了 p
的作用域,这是不好的。
此外,函数 is_valid
的名称可能会因使用的智能指针的类型而有所不同,它也可以是 is_empty
, Empty
, Valid
或设计者在创建它时可能想到的任何其他名称。
最后,即使不考虑命名问题和声明范围问题,对于智能指针,也存在非常实际的要求来支持类似指针的使用。通常应该可以在转换现有代码以使用智能指针而不是原始指针,并且对代码库的更改最少
例如,无论指针的智能性如何,这样的代码都可以正常使用:
template <typename T>
void some_func(const T& t) {
if (t)
t->print();
}
如果不将其转换成布尔可判断的类型,则上述 if 语句将无法针对智能指针进行编译,我们在本文中着手完成的目标是使转换安全。正如我们将看到的,这比乍一看要想象的更难一些
补充:我这里先总结一下,安全的 bool
转换具体包括哪些需求,后面提到一些解决方案分别完成了哪些需求
阻止相同类型比较
Testable b1,b2; if (b1==b2) {} if (b1<b2) {}
- 阻止不同类型比较
Testable test; Testable test2; if (test1==test2) {} if (test!=test2) {}
阻止无意义的类型转化
Testable test; int i = test;
- 阻止无意义的算数运算
Testable test; int i = test + 1;
阻止不合法的内存操作
Testable test; delete test;
- 代码尽量简洁明了
显而易见的方式
operator bool
重载隐式类型转化,代码如下:
// operator bool 版本 class Testable { bool ok_; public: explicit Testable(bool b=true):ok_(b) {} operator bool() const { return ok_; } };
注意转换函数的实现:
operator bool() const {
return ok_;
}
现在,我们可以在这样的表达式中使用该类的实例:
Testable test;
if (test)
std::cout << "Yes, test is working!\n";
else
std::cout << "No, test is not working!\n";
很好,但是有一个令人讨厌的警告,因为转换函数刚刚告诉编译器它可以自由的做我们背后的事情(永远不要信任编译器为您完成工作,至少不能正确的完成工作):
test << 1;
int i=test;
这些都是无意义的操作,但是这些仍然是能编译通过的合法的C++代码
此外,我们也可以将使用这种技术的任何类型相互比较,尽管这很少有道理:
Testable a;
AnotherTestable b;
if (a==b) {}
if (a<b) {}
因此 operator bool
不是一个很好的方法
我们还能做什么?好吧,一种改进方案是,添加另一个私有的转换成整数类型函数,从而禁止了无意义的操作,即使是相等和有序的操作也是如此,只需要将私有转换函数声明为 int 就可以了,但是仍然存在一些缺陷,是解决方案不尽人意。用户调用歧义性时的错误信息不一致或不可读,同样,这些转换功能可能会干扰完全有效的转换和重载,因此,我们必须在其他地方寻求针对该问题的干净解决方案
该方式除了 5 (不合法的内存操作), 6 (使用起来简洁明了),能在编译期间阻止的功能一个也没有实现
不太明显的方式 operator!
现在是时候使用 operator!
来实现更安全的转换
程序员已经习惯于在布尔判断中使用一元逻辑求反(!)运算符,这对于直观使用来说是理想的属性。
不过,某些用户可能还没有对这种 double-bang trick
技巧做好准备,但这是提供安全转换的前提和要求。
先看实现,实现起来很简单
// operator! 版本
class Testable {
bool ok_;
public:
explicit Testable(bool b=true):ok_(b) {}
bool operator!() const {
return !ok_;
}
};
这是一种更好的方法-不再需要担心隐式转换或重载问题,以及两种测试 Testable 的惯用方式:
Testable test;
if (!!test)
std::cout << "Yes, test is working!\n";
if (!test2) {
std::cout << "No, test2 is not working!\n";
if(!!test)
利用了一个很有用的技巧,它有时被称为 double-bang trick
把戏,但是它并没有 if(test)
那么优雅和简单
double-bang trick
: 这是一个古老的C技巧,用于将非零值映射到数字1,因此您可以将数字整数值映射到二进制值索引(0或1),从而与大小2的数组一起使用
很可惜,因为如果人们不了解某件事情的工作原理,那么是否安全并不重要,它仍然是一种非常有用的技术,但通常将在 普通 开发者从未看到的库代码中使用
当然,这种写法仍然可以比较不同的类型,就像第一种方式一样(尽管语法晦涩难懂,但这样做几乎没有意思)
Testable a;
AnotherTestable b;
if (!a==!b) {}
if (!a<!b) {}
该方式是可以提供在编译期间就报错的部分功能,但是用法过于别扭
看似合法的方式 operator void*
这是一个聪明的注意-使用转换成 void*
的函数,因为除了可以进行布尔判断之外,使用 void*
可以做的事情并不多,实现方式如下:
// operator void* 版本
class Testable {
bool ok_;
public:
explicit Testable(bool b=true):ok_(b) {}
operator const void*() const{
return ok_==true ? this : 0;
}
};
这个方案也是存在缺陷的,例如问题在于现在可以执行以下的操作:
Testable test;
delete test;
哎哟! 如果您认为可以用一些const技巧来解决这种情况,请再考虑一遍:C ++ Standard 明确允许使用delete指向const类型的指针的表达式。 这项技术最著名的用途也许来自C++标准。允许使用iostream状态进行查询的转换。 但是,虽然意图是这样;
if (std::cout) {
}
这里我在额外补充几点我编译过程中的遇到的现象
原文中的例子下面这样的,少一个
const
operator void*() const{ return ok_==true ? this : 0; }
我在编译的过程中会报错
error: invalid conversion from 'const void*' to 'void*' [-fpermissive]
大致意思就是不支持强制类型转化,如果需要强制类型转换添加编译条件-fpermissive
- 编译以下代码的时候不会报错,但是会有警告
warning: deleting 'const void*' is undefined [-Wdelete-incomplete]
Testable test; delete test;
- 编译以下代码的时候不会报错,但是会有警告
该方式因为返回的是
void *
, 所以在编译期间阻止了无意义的类型转化和算数运算,但是相同和不同间的类型比较并没有阻止, delete 操作会有警告但是也没有阻止
几乎完美的方式 嵌套类
1996年,Don Box 在他的 C++
报告专栏中写了一种非常聪明的技术-一种最初创建用于支持空性测试的技术-几乎可以完成我们在此所做的工作。它涉及到嵌套类型(甚至不需要定义)的转换函数,如下所示:
// 嵌套类的版本
class Testable {
bool ok_;
public:
explicit Testable(bool b=true):ok_(b) {}
class nested_class; // 这个类没有被定义,如果定义了话,delete 时不会有任何警告
operator const nested_class*() const {
return ok_ ? reinterpret_cast<const nested_class*>(this) : 0;
}
};
虽然它解决了很多问题,但是我们同样可以指出一个反例:
Testable b1,b2;
if (b1==b2) {}
if (b1<b2) {}
相对于
void *
, 返回的是一个内部类的地址,所以不同类之间比较的问题在编译期间可以阻止,但是对于相同类型之间的比较,仍然无法在编译期间阻止。
这里强调一点,因为内部类nested_class
是一个不完全类型,delete
的时候只会有警告不会报错
安全的布尔类型转换
现在是时候提供一个安全的布尔类型转换,我们需要避免导致错误的使用带来的不安全,无意义的类型转换,我们也必须奥避免重载引入的问题,以及不应该允许delete 操作。具体代码如下:
class Testable {
bool ok_;
typedef void (Testable::*bool_type)() const;
void this_type_does_not_support_comparisons() const {} // 私有函数
public:
explicit Testable(bool b=true):ok_(b) {}
operator bool_type() const {
return ok_==true ? &Testable::this_type_does_not_support_comparisons : 0;
}
};
我们定义了一个 bool_type
的零参数并且返回 void
的函数指针,指向 Testable
的 const
成员函数,我们提供了“魔术”类型,它允许进行布尔判断,并且没有多余实现其他的重载,接下来我们需要为 bool_type
定义一个成员函数 this_type_does_not_support_comparisons
, 当为 true
时,返回指向该成员函数的指针,当为 false
时, 返回 0。 现在可以在安全的使用布尔判断了
- 相对于
operate bool
我们避免了重载问题,以及避免了不安全的(int)类型转换问题 - 相对于
operate!
, 没有添加一些不常用的使用方式,保证了使用上的简洁性 - 相对于
void *
, 禁止了潜在的删除问题
但现在还需要另外一种方法使这个方案更加完善,现在只剩下 Testable
不同实例之间的比较这个问题
Testable test;
Testable test2;
if (test1==test2) {}
if (test!=test2) {}
补充一下: 因为对于指向类的成员函数的指针,只是支持判断相等不相等,不支持判断大于小于等运算。所以部分的满足了
Testable
不同实例之间的比较
像上面这样的比较不仅没有意思,而且很危险,因为它们暗示了在 Testable
的不同实例之间可能存在的等效关系,所以我们需要找到一种方法禁用这些无意义的比较
template <typename T>
bool operator!=(const Testable& lhs,const T& rhs) {
lhs.this_type_does_not_support_comparisons();
return false;
}
template <typename T>
bool operator==(const Testable& lhs,const T& rhs) {
lhs.this_type_does_not_support_comparisons();
return false;
}
通过定义非成员 opeartor ==
和 operator !=
函数,并让他们尝试调用 Textable
对象里的私有函数 this_type_does_not_support_comparisons
从而导致编译错误,从而禁止没有意义的比较。当然如果您要允许比较的测试,只需要照常定义比较运算符即可
这其实就是安全的布尔转化用法,当人们开始使用此方法的时候,发现某些编译器会降低效率-成员函数指针导致编译器头疼,导致在获取地址时速度变慢,尽管差异很小,但当前最合理的做法通常是使用成员数据指针而不是成员函数指针
补充一下文章没有提到的部分
成员数据指针: 指向类数据成员的指针
指向非静态数据成员的指针在定义时必须和类相关联,在使用时必须和具体的对象关联。
定义: <数据类型> <类名>::*<指针名>
赋值&初始化: <数据类型> <类名>::*<指针名>[=&<类名>::<非静态数据成员>]
如果换成成员数据指针,代码如下
class Testable {
int member = 1;
typedef int Testable::*bool_type;
public:
operator bool_type() {
return ok_ ? &Testable::member : nullptr;
}
private:
bool ok_;
};
定义成员数据指针 bool_type
, 类型是 int
, 提供一个成员变量 member
并初始化 int member=1
,进行布尔判断的时候如果为 true
则返回 &Testable::member
, 反之返回 nullptr
其实到这里关于安全的布尔类型转化的问题已经算是研究透了,本片博客翻译的原文后面还有一部分,关于如何实现 可复用的Safe Bool Idiom
, 大致意思就是:不想在每次实现需要提供可布尔判断的类时候都遵循上述步骤全部重写一遍的话,可以通过实现一个基类的形式去除掉重复的代码,具体的代码和一些解释性的说明,我就不翻译了,感兴趣的可以自己看一下原文 https://www.artima.com/cppsource/safebool.html
个人总结
- 不直接返回
bool
类型是因为bool
类型可以隐式转换为算术类型如整型,浮点型等,所以较易被误用 - 所以先转换成一个中间过渡的类型,然后从这个中间类型在隐式转换成
bool
类型,所以需要找一个可以隐式转换成bool
类型的类型 - 根据C++标准中提到的:算术,枚举,指针,和指向成员指针的右值都可以转换成bool类型右值。又因为算数,枚举,指针类型更容易误用,所以最后选择了指向成员指针(指向成员函数指针,指向成员数据指针)
- 又因为编译器在处理指向成员函数指针速度相对于指向成员数据指针慢一些,所以最后选择指向成员数据指针,并将该成员对象设计成私有,避免成员被访问赋值等操作