安全地转换成bool方法

来源:互联网 发布:网络的图标 png格式 编辑:程序博客网 时间:2024/05/10 05:58

第一次翻译水平很差,见谅.

原文地址:http://www.artima.com/cppsource/safebool.html

概要:

C++,有一些为类对象提供布尔测试的方法.这些方法要么提供直观的用法,要么支持泛型编程.我们将会看到4种为惯用的if(object){}结构提供支持的方法.最后,我们将会讨论一种新的解决方法,这种方法没有前面4种方法的缺陷.

目标

有些类型,像是指针,允许我们测试在布尔上下文下的有效性.任何算数右值,枚举类型,指针,或者指向成员的指针,可以被隐式地转换成布尔值.我们常常用这种属性选择一个代码执行分支,举个例子,当获取资源的时候:

if (some_type* p=get_some_type()) {// p is valid, use it}else {// p is not valid, take proper action}

当然,这种用法不仅对于内置类型有用,任何有着明确有效性(转换成布尔类型有意义)的类型都可以获益于这种布尔转换.另一种用法则是使用成员方法,举个例子,测试smart指针的有效性:

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}

除了代码更冗长之外,这种方法不同于前一种方法的地方是,p需要被定义在它被使用的代码块之外.从可维护性方面讲,这并不好.而且, smart指针的实际类型的不同,方法is_valid也可能有着不同的语意,方法名也可以是is_empty,Empty, Valid或者一个有创意的设计者在创建这个方法时候可能想到的名字.最后,即使撇开命名问题和声明域问题,smart指针应该要支持类似普通指针的用法(->, * 操作符).通常应该在尽量少地改变现有代码的基础上,把现有代码转换成使用smart指针而不是普通指针的代码.不管指针的智能性怎么样,像下面的代码应该能够运行:

template <typename T> void some_func(const T& t) {if (t)    t->print();}

没有一些转换成布尔类型的方法,上述的if语句对于smart指针来说会便以失败.我们在这篇文章列出的目标是安全地将对象转换成布尔类型.正如我们将看到的,这可能比想象中难一点.

直观的方法:operator bool()

这个典型的方法有一个直观的实现.在这篇文章里我将会一直使用同一个类(Testable),如下:

// operator bool versionclass Testable {bool ok_;public:explicit Testable(bool b=true):ok_(b) {}operator bool() const {return ok_;}};// operator! versionclass Testable {bool not_ok_;public:explicit Testable(bool b=true):not_ok_(!b) {}bool operator!() const {return not_ok_;}};// operator void* versionclass Testable {bool ok_;public:explicit Testable(bool b=true):ok_(b) {}operator void*() const {return ok_==true ? this : 0;}};// nested class versionclass Testable {bool ok_;public:explicit Testable(bool b=true):ok_(b) {}class nested_class {};operator const nested_class*() const {return ok_ ? reinterpret_cast<const nested_class*>(this) : 0;}};

注意类型转换函数的实现:

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++中仍然是被允许和合法的(我们还要考虑使事情变得更坏的重载的问题.).所以operator bool()不是一个好地方.而且我们可以利用这种方法去比较任何类型(实现了operator bool),虽然这么做没有意义:

Testable a;AnotherTestable b;if (a==b) {}if (a<b) {}

我们还能做些什么?,一个改进方法是添加另一个转换成整数类型的函数(private),因此禁止了如相等性和比较等无意义的操作.只需声明一个私有的转换成int的类型转换函数就可以了.但是,依然存在的一些缺点使得这种方法并不令人满意.当用户调用有歧义的操作时,错误信息并不一致,难于阅读和调试.而且这些类型转换函数可能会干扰其他合法的类型转换函数和重载函数.所以我们应该换个方向寻找一个干净安全的解决方案.

并不是非常直观,operator!

是时候向安全之地前进了,使用operator!.程序员们已经习惯在布尔上下文中使用一元逻辑取反操作,这种取反操作是一种比较令人满意的直观用法.但是,一些人可能并不知道一种叫做double-bang的技巧,一种检查一个对象的是否处于良好状态的技巧,代码看起来很简单:

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,但是,,这并不像if(test)那么优雅和直接(这是一个老的C语言技巧,用来把非零值映射成1,然后就可以通过把整数值转换成01,做一个元素个数为2的数组的索引).这很遗憾,因为如果人们不知道这个一个东西是如何运作的,也就不会去关心是否安全.这依旧是一个有用的技巧,但是一般被用在一般用户看不到的程序库代码中.当然,这种方法还是可以在不同类型之间做比较(虽然它的晦涩的语法使得这么做没有什么意义),就像operator bool()一样.有没有比这更好的方法?

一个看起来无害的方法 operator void*

这有一个聪明的想法——使用转换成void*的类型转换函数.说它聪明是因为除了在布尔上下文中,并没有很多使用void*的地方,是这么实现的:

operator void*() const {return ok_==true ? this : 0;}

又是一个平凡的实现!不用担心,从这里开始没有平凡的东西了.你可能已经猜到了,这么做也是有缺陷的.问题就是现在可以这么做了:

Testable test;delete test;

哎呦!如果你认为这种情况可以用过const技巧,再仔细想想:C++标准明确表示允许delete操作符对const 指针进行操作.可能这种方法最有名的用法来自于C++标准,这种转换可以用来测试io流的状态.然后可能是想这么做:  

if (std::cout) { // Is the stream ok?  }

但是你可以做么做:

std::cout << std::cin << std::cout;

而且,使用这种方法意味着可以在布尔上下文中测试不同类型(只要实现了这种有缺陷的方法的类型).

使用内嵌类

1996,Don Box 在他的C++ 报告专栏发表了一项巧妙的技巧.一项原本用来测试无效性(nullness)的技巧.这技巧几乎达到了我们的目标.它包含转换成一个内嵌类的类型转换函数(甚至不用去定义这个内嵌类),看起来像是这样:

class Testable {    bool ok_;  public:    explicit Testable(bool b=true):ok_(b) {}    class nested_class;    operator const nested_class*() const {      return ok_ ? reinterpret_cast<const nested_class*>(this) : 0;    }  };

现在,这个版本可以支持布尔测试了,但是,,支持的太多了.我们现在可以写出如下的错误代码:

Testable b1,b2;if (b1==b2) {}if (b1<b2) {}

我们可以定义所涉及的类型转换函数为私有,禁用所有已经被激活的操作符,使得算法更符合我们的目的,但是还有一种更好的方法.

安全地转换成bool的方法

是时候使这些操作变得安全了.记得我们需要避免那些允许错误使用的不安全转换.我们同样也要避免重载问题,我们绝对不应该允许对转换结果进行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定义成一个指向Testableconst成员函数的指针,这个函数没有参数,返回值为void.这就是允许在布尔上下文中进行测试且不会出现在重载上下文中的奇妙的类型.然后我们定义一个转换成bool_type的类型转换函数,就像前面定义成boolvoid*类型的类型转换函数一样的做法.最后,如果oktrue返回指向成员函数(this_type_does_not_support_comparisons)的指针,如果okfalse则返回null.现在可以在布尔上下文中安全地进行测试了.这个成员函数的名字是有目的,从名字中就可以读出来,它不支持比较操作.

跟转换成bool的类型转换函数比起来,我们避免了重载问题,返回整型带来的副效果(使得一些无意义的操作合法,比较、相等性).我们添加了晦涩的operator!拥有的效果(double_gang: !!),并且禁止了可能会发生的对void*进行delete的操作.很厉害吧!不过还需做一些额外的工作使得这个类完整,就是禁止Testable对象之间的比较.目前的实现,我们可以这么做:

Testable test;Testable test2;if (test1==test2) {}if (test!=test2) {}

上面的比较操作是无意义的,而且很危险,因为这样暗指了两个不同的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;     }

当然!通过把operator==operator!=定义成非成员函数,使得尝试调用私有函数(this_type_does_not_support_comparisons)出现编译错误,这样就禁止了这种无意义的测试行为.(限于篇幅,上述两个函数中的第二个参数被略去了),使用模板参数化的实现,我们可以确保只有当实例化比较函数的时候才会出现错误当比较函数被实例化的时候,长长的成员函数就会成为编译错误信息的一部分,以便于定位和修正错误.为了测试,你会想要允许比较操作,只要像平常那样对比较操作进行简单的定义.

这就是安全地转换成bool的方法.当人们开始使用这种方法的时候,可能在某些编译器上出现一些效率缺陷——取成员函数指针的值的时候可能会使编译器头疼,从而使得运行效率变慢(生成低效的代码).虽然区别不大,但是目前值得实践的是使用成员变量指针代替成员函数指针.

一个可复用的解决方案

如果你和我一样,你不会想要在每次定一个类的时候都要重复上述步骤定义一个boolean Testable.你想要一些可复用的东西,你不会失望的!有两个貌似可行的解决方案:使用一个带有一个用来执行实际逻辑测试的virutal函数的基类,或者一个知道该调用哪个子类函数的基类.因为virtual函数带来了一些代价(特别是当你的类没有其他virtual函数的时候),下述的方案支持上述的两个方案:

class safe_bool_base {protected:typedef void (safe_bool_base::*bool_type)() const;void this_type_does_not_support_comparisons() const {}safe_bool_base() {}safe_bool_base(const safe_bool_base&) {}safe_bool_base& operator=(const safe_bool_base&) {return *this;}~safe_bool_base() {}};template <typename T=void> class safe_bool : public safe_bool_base {public:operator bool_type() const {return (static_cast<const T*>(this))->boolean_test()? &safe_bool_base::this_type_does_not_support_comparisons : 0;}protected:~safe_bool() {}};template<> class safe_bool<void> : public safe_bool_base {public:operator bool_type() const {return boolean_test()==true ? &safe_bool_base::this_type_does_not_support_comparisons : 0;}protected:virtual bool boolean_test() const=0;virtual ~safe_bool() {}};template <typename T, typename U> void operator==(const safe_bool<T>& lhs,const safe_bool<U>& rhs) {lhs.this_type_does_not_support_comparisons(); return false;}template <typename T,typename U> void operator!=(const safe_bool<T>& lhs,const safe_bool<U>& rhs) {lhs.this_type_does_not_support_comparisons();return false;   }//Here's how to use safe_bool:class Testable_with_virtual : public safe_bool<> {protected:bool boolean_test() const {// Perform Boolean logic here}};class Testable_without_virtual : public safe_bool <Testable_without_virtual> {public:bool boolean_test() const {// Perform Boolean logic here}};

第一个类Testable_with_virtual,公有地继承自safe_bool,并且实现了virtual方法boolean_test, 这个方法在实例在布尔上下文中被测试的时候被调用(if(obj){}, if(!obj){}).第二个类,Testable_without_virtual,同样公有地继承自safe_bool,除此之外,它把自己作为模板参数传给了基类.这个小技巧——被称为Curiously Recurring Template Pattern——使得基类使用static_cast向下转换成子类并且调用boolean_test,这么做没有额外的运行时间代价和virtual调用.有些人可能会感觉这有点误用继承,但是我也可以称子类和基类的关系是 子类is-a safe_bool,这当然不是这段代码的目的.然而,几乎没有理由去相信没经验的程序员会掉进误解这种关系的陷阱中.safe_bool的析构函数被定义成protected来避免误用,可能某些小心谨慎的面向对象纯粹追求者使用私有继承,然后把类型转换函数重新引入到子类中.

class Testable_without_virtual : private safe_bool <Testable_without_virtual> {public:using safe_bool<Testable_without_virtual>::operator safe_bool;bool boolean_test() const {return true; // Logic goes here!}};

Matthew Wilson指出这种继承策略(使用safe_bool作为基类)可能会导致在某些编译器上出现空间惩罚,特别是在那些没有适当实现EBO(Empty Base Optimization)的编译器上. 尽管大多数现代编译器会在单继承的时候会做这种优化,但是在多继承的时候会带来一些空间惩罚.

什么时候应该说不?

使得这个技巧很酷,你可能很想在你的某些类中应用它.在行动之前,很有必要去知道这个技巧应该只被使用在有着合理的有效性的概念的类上(转换成bool类型有意义的类).考虑iostreamvoid*类型转换函数.你知道哪些状态在测试(类似if(cout)的操作)的时候被考虑进去?大多数人认为他们知道.我查阅了相关资料发现至少我是错了——eofbit状态位被忽略了.这表明了如果程序员对语意的期望不同,提供一个有意义的名字的成员函数将会更好.前面的iostream例子中,提供一个fail()函数来测试错误,提供!good()来表示输入错误 是非常合理的.对于容器类来说,定义一个类型转换函数无异于灾难,因为容器可能的解释实在太多了.对于大多数的类来说,设计测试有效性的合理方式是提供有着有意义名字的成员函数,而不是类型转换函数.就这样吧.



原创粉丝点击