文章目录
- 类型转换运算符
- 概念
- 避免过度使用类型转换函数
- 解决上述问题的方法
- 转换为 bool
- 显式的类型转换运算符
- 类型转换二义性
- 重载函数与类型转换结合导致的二义性
- 重载运算符与类型转换结合导致的二义性
类型转换运算符
概念
类型转换运算符(conversion operator)是类的一种特殊成员函数。负责将一个类类型的值转换成其他类型。
operator type() const ;
其中 type
表示某种类型。类型转换运算符可以面向任意类型(除了 void
之外)进行定义,只要该类型能作为函数的返回类型。因此,我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。
一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常不应该改变待转换对象的内容,因此,应该是const。
运用实例,定义一个简单的类,令其表示 0~255
之间的一个整数:
构造函数将算术类型的值转换成 SmallInt
对象,而类型转换运算符将 SmallInt
对象转换成 int
:
SmallInt si;
si = 4; // 将 4 隐式转换成 SmallInt,然后调用 SmallInt::operator=
si + 3; // 首先将 si 隐式地转换成 int,然后执行整数的加法
尽管编译器一次只能执行一个 我们定义的类型转换(如上面的构造函数/类型转换运算符),但可以将其搭配 内置类型转换(如double可以转换成int) 实现二次转换。
// 内置类型转换将 doulbe 实参转换成 int
SmallInt si = 3.14; // 调用 SmallInt(int) 构造函数,然后调用拷贝构造函数
// SmallInt 的类型转换运算符将 si 转换成 int
si + 3.14; // 内置类型将所得的 int 继续转换成 double
尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值:
避免过度使用类型转换函数
- 类型转换可能具有误导性
例如,假设某个类表示 Date
,我们也许会为它添加一个从 Date
到 int
的转换。然而,类型转换函数的返回值应该是什么?
- 一种可能的解释是,函数返回一个十进制数,依次表示年、月、日,例如,
July 30,1989
可能转换为int
值19890730
。 - 同时还存在另外一种合理的解释,即类型转换运算符返回的
int
表示的是从某个时间节点(比如January 1,1970
)开始经过的天数。
问题在于 Date
类型的对象和 int
类型的值之间不存在明确的一对一映射关系。因此在此例中,不定义该类型转换运算符也许会更好。作为替代的手段,类可以定义一个或多个普通的成员函数以从各种不同形式中提取所需的信息。
- 类型转换运算符可能产生意外结果
对于类来说,定义向 bool
的类型转换还是比较普遍的现象。
int i = 42;
cin << i; // 如果向 bool 的类型转换不是显式的,则该代码在编译器看来是合法的
因为 istream
本身并没有定义 <<
,所以本来代码应该产生错误。然而,该代码能使用 istream
的 bool类型转换运算符
将 cin
转换成 bool
,而这个 bool值
接着会被提升成 int
并用作内置的左移运算符的左侧运算对象。这样一来,提升后的 bool值(1或0)
最终会 被左移42个位置。 这一结果显然与我们的预期大相径庭。
解决上述问题的方法
转换为 bool
- 标准库的早期版本中,
IO
类型定义了向void*
的转换规则,以求避免上述问题。 - 在
C++11
标准中,IO
标准库通过定义一个向bool
的显式类型转换实现同样的目的。
其实我们在编程中经常用到 IO
类型定义的 operator bool
:
while(std::cin >> value)
为了对条件求值,cin
被 istream operator bool
类型转换函数隐式地执行了转换。如果 cin
的条件状态是 good
,则该函数返回为真;否则该函数返回为假。(这部分知识可以看我之前的博客)
向 bool
的类型转换通常用在条件部分,因此 operator bool
一般定义成 explicit
的。
显式的类型转换运算符
为了防止上面第二点这样的异常情况发生,我们可以使用 explicit
关键字。
SmallInt si = 3; // 正确:SmallInt 的构造函数不是显式的
si + 3; // 错误:explicit阻止隐式类型转换
static_cast<int>(si) + 3; // 正确:显式地请求类型转换
当类型转换运算符是显式的时,我们也能执行类型转换,不过必须通过显式的强制类型转换才可以。
该规定存在一个例外,即,如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它。 换句话说,当表达式出现在下列位置时,显式的类型转换将被隐式地执行:
if
、while
及do
语句的条件部分for
语句头的条件表达式- 逻辑非运算符(
!
)、逻辑或运算符(||
)、逻辑与运算符(&&
)的运算对象 - 条件运算符(
? :
)的条件表达式。
类型转换二义性
如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则的话,我们编写的代码将很可能会具有二义性。
在两种情况下可能产生多重转换路径:
- 第一种情况是 两个类提供相同的类型转换: 例如,当
A类
定义了一个接受B类
对象的转换构造函数,同时B类
定义了一个转换目标是A类
的类型转换运算符。 - 第二种情况是 类定义了多个转换规则,而某些转换规则可以通过其他类型转换实现。 这种情况多出现在算术运算符上。
通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算术类型的转换。
第一种情况举例:
解决方法是显式调用:
A a1 = f(b.operator A());
A a2 = f(A(b));
第二种情况举例:
我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个:
short s = 42;
// 把 short 提升成 int 优于 提升成 double
// 上面的 long 则没有int和double谁优于谁的规则,因此会有二义性
A a3(s); // A::A(int)
重载函数与类型转换结合导致的二义性
有时会出现这种情况:
或这种情况:
虽然我们可以通过显式地构造正确的类型而消除二义性:
manip(C(10)); // 调用 manip(const C&)
manip2(E(double(10))); // 调用 manip2(const E&)
但意味着程序的设计存在不足。
重载运算符与类型转换结合导致的二义性
重载的运算符也是重载的函数。因此也遵从通用的函数匹配规则。例如,如果 a
是一种类类型,则表达式 a sym b
可能是:
a.operatorsym(b); // a 有一个 operatorsym 成员函数
operatorsym(a, b); // operatorsym 是一个普通函数
和普通函数不同,我们无法通过调用的形式区分当前调用的是成员函数还是非成员函数。
举个例子:
- 第一条加法语句接受两个
SmallInt
值并执行+
运算符的重载版本。 - 第二条加法语句具有二义性:因为我们可以把
0
转换成SmallInt
,然后使用SmallInt
的+
;或者把s3
转换成int
,然后对于两个int
执行内置的加法运算。
如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。