《More Effective C++》 条款5 谨慎定义类型转换函数

---恢复内容开始---

C++编译器能够在两种数据类型之间进行隐式转换(implicit
conversions),它继承了C语言的转换方法,例如允许把char隐式转换为int和从short隐式转换为double。因此当你把一个short值传递给准备接受double参数值的函数时,依然可以成功运行。C中许多这种可怕的转换可能会导致数据的丢失,它们在C++中依然存在,包括int到short的转换和double到char的转换。

你对这些类型转换是无能为力的,因为它们是语言本身的特性。不过当你增加自己的类型时,你就可以有更多的控制力,因为你能选择是否提供函数让编译器进行隐式类型转换。

有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument
constructors)和隐式类型转换运算符。单参数构造函数是指只用一个参数即可以调用的构造函数。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的所有参数都有缺省值。以下有两个例子:

class Name { // for names of things

public:

Name(const string& s); // 转换 string 到

// Name

...

};

class Rational { // 有理数类

public:

Rational(int numerator = 0, // 转换int到

int denominator = 1); // 有理数类

...

};

隐式类型转换运算符只是一个样子奇怪的成员函数:operator
关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。例如为了允许Rational(有理数)类隐式地转换为double类型(在用有理数进行混合类型运算时,可能有用),你可以如此声明Rational类:

class Rational {

public:

...

operator double() const; // 转换Rational类成

}; // double类型

在下面这种情况下,这个函数会被自动调用:

Rational r(1, 2); // r 的值是1/2

double d = 0.5 * r; // 转换 r 到double,

// 然后做乘法

完整示例:

#include<iostream>
using namespace std;

class Rational{
public:
Rational(int numerator=0,int denominator=1):n(numerator),d(denominator){}

operator double() const
{
return (double)n/d;
}
private:
int n,d;
friend ostream& operator<<(ostream& s,const Rational& r);

};

ostream& operator<<(ostream& s,const Rational& r)
{
s<<r.n<<"/"<<r.d;
return s;
}
int main()
{
Rational r(1,2);
cout<<r<<endl;
double d=0.5*r;
cout<<d<<endl;

}

以上这些说明只是一个复习,我真正想说的是为什么你不需要定义各中类型转换函数。

根本问题是当你在不需要使用转换函数时,这些的函数缺却能被调用运行。结果这些不正确的程序会做出一些令人恼火的事情,而你又很难判断出原因。

让我们首先分析一下隐式类型转换运算符,它们是最容易处理的。假设你有一个如上所述的Rational类,你想让该类拥有打印有理数对象的功能,就好像它是一个内置类型。因此,你可能会这么写:

Rational r(1, 2);

cout << r; // 应该打印出"1/2"

再假设你忘了为Rational对象定义operator<<。你可能想打印操作将失败,因为没有合适的的operator<<被调用。但是你错了。当编译器调用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

在多数情况下,这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再会发生,这点损失是值得的。一般来说,越有经验的C++程序员就越喜欢避开类型转换运算符。例如在C++标准库(参见条款49和35)委员会工作的人员是在此领域最有经验的,他们加在库函数中的string类型没有包括隐式地从string转换成C风格的char*的功能,而是定义了一个成员函数c_str用来完成这个转换,这是巧合么?我看不是。

通过单参数构造函数进行隐式类型转换更难消除。而且在很多情况下这些函数所导致的问题要甚于隐式类型转换运算符。

举一个例子,一个array类模板,这些数组需要调用者确定边界的上限与下限:

template<class T>

class Array {

public:

Array(int lowBound, int highBound);

Array(int size);

T& operator[](int index);

...

};

第一个构造函数允许调用者确定数组索引的范围,例如从10到20。它是一个两参数构造函数,所以不能做为类型转换函数。第二个构造函数让调用者仅仅定义数组元素的个数(使用方法与内置数组的使用相似),不过不同的是它能做为类型转换函数使用,能导致无穷的痛苦。

例如比较Array<int>对象,部分代码如下:

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]"

do something for when

a[i] and b[i] are equal;

}

else {

do something for when they‘re not;

}

我们想用a的每个元素与b的每个元素相比较,但是当录入a时,我们偶然忘记了数组下标。当然我们希望编译器能报出各种各样的警告信息,但是它根本没有。因为它把这个调用看成用Array<int>参数(对于a)和int
(对于b[i])参数调用operator==函数
,然而没有operator==函数是这些的参数类型,我们的编译器注意到它能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int
类型的参数。然后编译器如此去编译,生成的代码就象这样:

for (int i = 0; i < 10; ++i)

if (a == static_cast< Array<int> >(b[i])) ...

每一次循环都把a的内容与一个大小为b[i]的临时数组(内容是未定义的)比较
。这不仅不可能以正确的方法运行,而且还是效率低下的。因为每一次循环我们都必须建立和释放Array<int>对象(见条款19)。

通过不声明运算符(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(参见条款2),两个“>”字符间的空格不能漏掉,如果这样写语句:

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>。这种转换顺序被禁止的,所以当试图进行比较时编译器肯定会产生错误。

ArraySize类的使用有些象一个有目的的帮手,这是一个更通用技术的应用实例。类似于ArraySize的类经常被称为proxy
classes,因为这样类的每一个对象都为了支持其他对象的工作。ArraySize对象实际是一个整数类型的替代者,用来在建立Array对象时确定数组大小。Proxy对象能帮你更好地控制软件的在某些方面的行为,否则你就不能控制这些行为,比如在上面的情况里,这种行为是指隐式类型转换,所以它值得你去学习和使用。你可能会问你如何去学习它呢?一种方法是转向条款33;它专门讨论proxy
classes。

在你跳到条款33之前,再仔细考虑一下本条款的内容。让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。

时间: 2024-10-10 17:58:05

《More Effective C++》 条款5 谨慎定义类型转换函数的相关文章

More Effective C++----(4)避免无用的缺省构造函数 &amp; (5)谨慎定义类型转换函数

Item M4:避免无用的缺省构造函数 缺省构造函数(指没有参数的构造函数)在C++语言中是一种让你无中生有的方法.构造函数能初始化对象,而缺省构造函数则可以不利用任何在建立对象时的外部数据就能初始化对象.有时这样的方法是不错的.例如一些行为特性与数字相仿的对象被初始化为空值或不确定的值也是合理的,还有比如链表.哈希表.图等等数据结构也可以被初始化为空容器. 但不是所有的对象都属于上述类型,对于很多对象来说,不利用外部数据进行完全的初始化是不合理的.比如一个没有输入姓名的地址簿对象,就没有任何意

Effective C++ -----条款35:考虑virtual函数以外的其他选择

virtual函数的替代方案包括NVI手法及Strategy设计模式的多种手法.NVI手法自身是一个特殊形式的Template Method设计模式. 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员. tr1::function对象的行为就想一般函数指针.这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities).

effective c++条款13-17 “以对象管理资源”之C++隐式转换和转换构造函数

其实我们已经在C/C++中见到过多次标准类型数据间的转换方式了,这种形式用于在程序中将一种指定的数据转换成另一指定的类型,也即是强制转换,比如:int a = int(1.23),其作用是将1.23转换为整形1.然而对于用户自定义的类类型,编译系统并不知道如何进行转换,所以需要定义专门的函数来告诉编译系统改如何转换,这就是转换构造函数和类型转换函数! 注意:转换构造函数.隐式转换和函数对象不要搞混淆!!!函数对象是重载运算符(),和隐式转换函数易混淆. 一.转换构造函数 转换构造函数(conve

第42课 类型转换函数

1. 类型转换函数 (1)C++类中可以定义类型转换函数 (2)类型转换函数用于将类对象转换为其它类型 (3)语法规则: operator Type() //重载类型运算符 { Type ret; //…… return ret; } [编程实验]类型转换函数初探 #include <iostream> using namespace std; class Test { int mValue; public: Test(){mValue = 0;} //转换构造函数 Test(int i =

第42课 类型转换函数(下)

问题: 类类型能否转换到普通类型? 示例如下: 可以看到上述程序中的强转是不成功的. 这时就需要引入新的语法规则了,类型转换函数: 添加类型转换函数: 运行结果如下: 分析: 上述程序中的int i = t相当于调用函数,int i = t.operator  int(). 类类型之间相互转换: 示例: 1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Test; 7 8 cla

C++中的类型转换函数

1,转换构造函数可以将普通的基础类型转换为当前的类类型,也有能力将其它类类 型的对象转换为当前的类类型: 2,问题: 1,类类型是否能够类型转换到普通类型? 1,可以的: 3,类型转换函数: 1,C++ 类中可以定义类型转换函数: 2,类型转换函数用于将类对象转换为其他类型: 1,不管什么类型都可以: 3,语法规则: 1,代码示例: 1 operator Type () // operator 表示定义类型转换函数,Type 表示返回值,没有参数: 2 { // 函数体表示普通的函数体内容: 3

C++ 类类型转换函数explicit 关键字

标准数据之间会进行  隐式类型安全转换. 转换规则: 隐式类型转换的问题: #include <iostream> #include <string> using namespace std; int main() { short s = 'a'; unsigned int ui = 1000; int i = -2000; double d = i; cout <<"d = "<<d<<endl;//输出d = -2000

Effective C++笔记_条款46 需要类型转换时请为模板定义非成员函数

看这个章节的时候又跑回去看了一下 条款 24,本章的例子就是基于下面这个例子而来的.在 item 24 中,支持混合运算的示例代码如下: 1 class Rational { 2 public: 3 Rational(int numerator = 0, int denominator = 1); 4 int mumerator() const; 5 int denominator() const; 6 private: 7 int numerator; 8 int denominator; 9

读书笔记 effective c++ Item 46 如果想进行类型转换,在模板内部定义非成员函数

1. 问题的引入——将operator*模板化 Item 24中解释了为什么对于所有参数的隐式类型转换,只有非成员函数是合格的,并且使用了一个为Rational 类创建的operator*函数作为实例.在继续之前建议你先回顾一下这个例子,因为这个条款的讨论是对它的扩展,我们会对Item 24的实例做一些看上去无伤大雅的修改:对Rational和opeartor*同时进行模板化: 1 template<typename T> 2 class Rational { 3 public: 4 Rati