类型转换运算符
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型,类型转换函数的一般形式:
operator type() const;
type
表示某种类型,类型转换运算符可以面向任意类型(除void之外)进行定义,只要该类型能够作为函数的返回类型,因此,不允许将转换成数组或者函数类型,但是允许转换成指针(包括:数组指针、函数指针)或者引用类型。
类型转换运算符既没有显示的返回类型,也没有形参,而且必须定义成类的成员函数,类型转换运算符通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成 const
成员。
定义含有类型转换运算符的类
class SmallInt{
public:
SmallInt(int i= 0):val(i)
{
if(i < 0 || i > 255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const{return val;}
private:
std::size_t val;
}
构造函数将算术类型的值转换成 SmallInt
,而类型转换运算符将 SmallInt
对象转换成 int
。
SmallInt si;
si = 4; //首先将4隐式转换成SmallInt,然后调用SmallInt::operator=
si += 3; //首先将si隐式地转换成int,然后执行整数加法
类型转换运算符可能产生意外结果
对于类来说,定义向 bool
类型转换是比较普遍的现象。
int i = 42;
cin << i;
上面的代码试图将输出运算符作用于输入流,因为 istream
本身没有定义 <<
,所以本来代码应该产生错误,然而,该代码能够使用 istream
的 bool
类型转换运算符将 cin
转换成 bool
。而这个 bool
值接着会被提升为 int 并用作内置的左移运算符的左侧运算对象。如此一来,提升后的 bool
值将被左移42个位置。
为了防止上述的转换发生,C++11 新标准引入了显式的类型转换运算符:
class SamllInt{
public:
explicit operator int() const{return val;}
//...
}
SmallInt si = 3; //正确,SamllInt的构造函数不是显式的
si += 3; //错误,这里需要隐式类型转换,但是类的运算符是显式的
static_cast<int>(si) + 3; //正确,显式的请求类型转换
但是当表达式被用作条件时,编译器会将显式的类型转换自动应用于它,即当表达式出现在下列位置时,显式的类型转换被隐式执行:
- if、while、do 语句的条件部分。
- for 语句头的条件表达式。
- 逻辑非(!)运算符,逻辑与(&&)运算符,逻辑或(||)运算符。
- 条件运算符(? : )的条件表达式。
转换为 bool
C++ 11 新标准下,无论何时在条件表达式中使用流对象,都会使用为 IO 类型定义的 operator bool
:
while(cin >> value)
输入运算符负责把数据读到 value
中,并返回 cin
,为了对条件求值,cin
被 istream operator bool
类型转换函数隐式地执行了转换,如果 cin
的条件状态是 good
,则函数返回真,否则返回假。
注意:
向 bool
的类型转换通常作用在条件部分,因此 operator bool
一般被定义成 explicit
的。
避免有二义性的类型转换
如果类中包含一个或多个类型转换,则必须保证在类类型和目标类型之间只存在唯一的一种转换方式,否则的话,有可能出现二义性。
两种情况下可能发生多重转换路径:
- 两个类提供相同的类型转换:当A类定义了接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符时,它们提供了相同的类型转换。
- 类定义了多个转换规则,而这些转换涉及的类型本身可以通过其它类型转换联系在一起,最典型的例子就是算术运算符,对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则。
注意:
通常不用为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或者转换目标是算术类型的转换。
实参匹配和相同的类型转换
struct B;
struct A
{
A() = default;
A(const B&); //把一个B转换为A
};
struct B{
operator A() const; //也是把B转换为A
}
A f(const A&);
B b;
A a = f(b); //二义性错误,含义是 f(B::operator A()) 还是f(A::A(const B&)) ???
如果确实要执行上面的调用需要显示的调用:
A a1 = f(b.operator A());
A a2 = f(A(b));
二义性转换目标为内置类型的多重类型转换
struct A{
//最好不要创建两个转换源都是算术类型的类型转换
A(int = 0);
A(double);
//最好不要创建两个转换对象都是算术类型的类型转换
operator int() const;
operator double() const;
};
void f2(long double);
A a;
f2(a); //二义性错误,含义是 f(A::operator int()),还是f(A::operator double())
long lg;
A a2(lg); //二义性错误,含义是A::A(int) 还是 A::A(double)
上面的转换因为不存在最佳匹配,所以会造成二义性。
short s = 42;
A a3(s); //使用 A::A(int),因为short转换成int由于short转换成double
重载函数与转换构造函数
struct C
{
C(int);
};
struct D
{
D(int);
};
void manip(const C&);
void manip(const D&);
manip(10); //二义性错误,含义是manip(C(10))还是manip(D(10))
manip(C(10)); //正确,显示的指明调用
重载函数与用户定义的类型转换
struct E
{
E(double);
};
void manip2(const C&);
void manip2(const E&);
//二义性错误,两个不同的用户定义的类型转换都能在此处使用
manip2(10); //含义是manip2(C(10))还是manip2(E((double)10))
因为调用重载函数所请求的用户定义的类型转换不止一个且彼此不同,所以该调用具有二义性,即使其中一个调用需要额外的标准类型转换而另一个调用能精确匹配,编译器也会提示错误。
函数匹配与重载函数
重载的运算符也是重载的函数,通用的函数匹配规则同样适用于判断给定的表达式中到底应该使用内置运算符还是重载的运算符,不过当运算符出现在表达式中时,候选函数集的规模要比我们使用调用运算符调用函数时更大。
表达式 a sym b
可能是:
//不能通过调用形式来区分当前调用的是成员函数还是非成员函数
a.operatorsym(b); //a有一个operatorsym 成员函数
operatorsym(a,b); //operatorsym 是一个普通函数
当使用重载运算符作用于类类型的对象时,候选函数中包含了该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。
当调用一个命名函数时,具有该名字的成员函数和非成员函数不会彼此重载,这是因为用来调用命名函数的语法形式对于成员函数和非成员函数来说是不同的。当通过类类型的对象进行函数调用时,只考虑该类的成员函数。而当在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数,两者都在考察范围内。
class SmallInt{
friend SmallInt operator + (const SmallInt&,const SmallInt&);
public:
SmallInt(int = 0);
operator int() const {return val;}
private:
std::size_t val;
};
SmallInt s1,s2;
SmallInt s3 = s1 + s2; //使用重载的 operator+
int i = s3 + 0; //二义性错误
第一条语句接受两个 SmallInt
值并执行 +
运算符的重载版本。
第二条加法语句具有二义性,因为可以把0转换成 SmallInt
,然使用 SmallInt
的 +
;或者把 s3
转换成 int
,然后对于两个 int
执行内置的加法。
注意:
如果对同一个类既提供了转换目标为算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。