More Effective C++ 阅读笔记(四)-- 避免使用隐式类型转换黑月亮 发表于 2005-10-3 15:38:00 让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。 隐式类型转换的缺点:它们的存在将导致错误的发生。例如:class Rational {public: ... operator double() const; // 转换Rational类成double类型};在下面这种情况下,这个函数会被自动调用:Rational r(1, 2); // r 的值是1/2 double d = 0.5 * r; // 转换 r 到double,然后做乘法假设你有一个如上所述的Rational类,你想让该类拥有打印有理数对象的功能,就好像它是一个内置类型。因此,你可能会这么写:Rational r(1, 2); cout << r; // 应该打印出"1/2"当编译器调用operator<<时,会发现没有这样的函数存在,但是它会试图找到一个合适的隐式类型转换顺序以使得函数调用正常运行。类型转换顺序的规则定义是复杂的,但是在现在这种情况下,编译器会发现它们能调用Rational::operator double函数来把r转换为double类型。所以上述代码打印的结果是一个浮点数,而不是一个有理数。这简直是一个灾难,但是它表明了隐式类型转换的缺点:它们的存在将导致错误的发生。 解决方法是用不使用语法关键字的等同的函数来替代转换运算符。例如为了把Rational对象转换为double,用asDouble函数代替operator double函数:class Rational {public: ... double asDouble() const; //转变 Rational}; // 成double这个成员函数能被显式调用:Rational r(1, 2); cout << r; // 错误! Rationa对象没有operator<< cout << r.asDouble(); // 正确, 用double类型打印r 通过不声明运算符(operator)的方法,可以克服隐式类型转换运算符的缺点,但是单参数构造函数没有那么简单。毕竟,你确实想给调用者提供一个单参数构造函数。同时你也希望防止编译器不加鉴别地调用这个构造函数。幸运的是,有一个方法可以让你鱼肉与熊掌兼得。事实上是两个方法:一是容易的方法,二是当你的编译器不支持容易的方法时所必须使用的方法。 容易的方法是利用一个最新编译器的特性,explicit关键字。为了解决隐式类型转换而特别引入的这个特性,它的使用方法很好理解。构造函数用explicit声明,如果这样做,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法:template<class T>class Array {public: ... explicit Array(int size); // 注意使用"explicit" ...}; Array<int> a(10); // 正确, explicit 构造函数在建立对象时能正常使用Array<int> b(10); // 也正确 if (a == b[i]) ... // 错误! 没有办法隐式转换int 到 Array<int> if (a == Array<int>(b[i])) ... // 正确,显式从int到Array<int>转换(但是代码的逻辑不合理)if (a == static_cast< Array<int> >(b[i])) ... //同样正确,同样不合理 if (a == (Array<int>)b[i]) ... //C风格的转换也正确,但是逻辑 依旧不合理 在例子里使用了static_cast(参见条款M2),两个“>”字符间的空格不能漏掉,如果这样写语句:if (a == static_cast<Array<int>>(b[i])) ... 这是一个不同的含义的语句。因为C++编译器把“>>”做为一个符号来解释。在两个“>”间没有空格,语句会产生语法错误。 如果你的编译器不支持explicit,你不得不回到不使用成为隐式类型转换函数的单参数构造函数。 我前面说过复杂的规则决定哪一个隐式类型转换是合法的,哪一个是不合法的。这些规则中没有一个转换能够包含用户自定义类型(调用单参数构造函数或隐式类型转换运算符)。你能利用这个规则来正确构造你的类,使得对象能够正常构造,同时去掉你不想要的隐式类型转换。 再来想一下数组模板,你需要用整形变量做为构造函数参数来确定数组大小,但是同时又必须防止从整数类型到临时数组对象的隐式类型转换。你要达到这个目的,先要建立一个新类ArraySize。这个对象只有一个目的就是表示将要建立数组的大小。你必须修改Array的单参数构造函数,用一个ArraySize对象来代替int。代码如下:template<class T>class Array {public: class ArraySize { // 这个类是新的 public: ArraySize(int numElements): theSize(numElements) {} int size() const { return theSize; } private: int theSize; }; Array(int lowBound, int highBound); Array(ArraySize size); // 注意新的声明 ... }; 这里把ArraySize嵌套入Array中,为了强调它总是与Array一起使用。你也必须声明ArraySize为公有,为了让任何人都能使用它。 想一下,当通过单参数构造函数定义Array对象,会发生什么样的事情: Array<int> a(10); 你的编译器要求用int参数调用Array<int>里的构造函数,但是没有这样的构造函数。编译器意识到它能从int参数转换成一个临时ArraySize对象,ArraySize对象只是Array<int>构造函数所需要的,这样编译器进行了转换。函数调用(及其后的对象建立)也就成功了。 事实上你仍旧能够安心地构造Array对象,不过这样做能够使你避免类型转换。考虑一下以下代码:bool operator==( const Array<int>& lhs, const Array<int>& rhs);Array<int> a(10);Array<int> b(10);...for (int i = 0; i < 10; ++i) if (a == b[i]) ... // 哎呦! "a" 应该是 "a[i]"; // 现在是一个错误。 为了调用operator==函数,编译器要求Array<int>对象在”==”右侧,但是不存在一个参数为int的单参数构造函数。而且编译器无法把int转换成一个临时ArraySize对象然后通过这个临时对象建立必须的Array<int>对象,因为这将调用两个用户定义(user-defined)的类型转换,一个从int到ArraySize,一个从ArraySize到Array<int>。这种转换顺序被禁止的,所以当试图进行比较时编译器肯定会产生错误。