C++重载运算与类型转换:类型转换运算符,及相关联的二义性问题

重载、类型转换与运算符

​ 我们知道,当类的构造函数只有一个形参时,我们可以隐式的进行类型转换,这种构造函数将实参类型的对象转换成类类型。我们同样能定义对于类类型的类型转换,通过定义类型转换运算符可以做到这一点。转换构造函数和类型转换运算符共同定义了类类型转换,这样的转换有时也被称作用户定义的类型转换

类型转换运算符

类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:

operator type() const;

其中 type 表示某种类型。类型转换运算符可以面向任意类型(void 除外)进行定义,只要该类型能作为函数的返回类型。因此,我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针和函数指针)或者引用类型。

类型转换运算符没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。通常情况下,类型转换运算符不应该改变待转换对象的内容,因此,类型转换运算符一般被定义为 const 成员。

定义含有类型转换运算符的类

​ 我们定义一个类,令其表示 0 到 255 之间的一个整数:

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 对象,而类型转换运算符将 SmallInt 对象转换从 int。所以我们可以这样使用这个类:

SmallInt si = 4;
cout << si + 3 << endl;

​ 尽管编译器一次只能执行一个用户定义的类类型转换但是这个转换可以和标准(内置)转换一起使用。例如,我们可以将任何可转换成 int 的类型传给 SmallInt 的构造函数。同样,我们也可以使用类型转换运算符将 SmallInt 转换成 int,然后转换成其他类型。

SmallInt si = 3.14;		// 或者 'c'
cout << si + 3.14;		// 输出 6.14

建议:避免过度使用类型转换函数

类型转换运算符可能产生意外结果

​ 一般情况下,类很少提供类型转换运算符。但是对类定义向 bool 类型的转换还是比较普遍的。

​ 在早期 C++ 的版本中,类定义向 bool 的转换常常会遇到一个问题:因为 bool 是一种算术运算,所以类类型的对象转换成 bool 后可以被用在任何需要算术类型的上下文中。这样的类型转换可能会引发意想不到的结果,如当 istream 含有向 bool 的类型转换时,下面的代码仍将编译通过:

int i = 10;
cin << i;

我们知道 istream 没有定义 <<,但如果 istream 可以向 bool 转换,这里将会编译通过,此语句就变成了 1 << i,即数字 1 被左移了 i 位。这一结果与我们的预期大庭相径。

显示的类型转换运算符

​ 为了防止上述情况的发生,C++11引入了显示的类型转换运算符

class SmallInt {
public:
    // 编译器不会自动执行这一类型转换
    explicit operator int() const { return val; }
    // 其他成员与之前一致
};

和显示的构造函数一样,编译器通常也不会将一个显示的类型转换运算符用于隐式类型转换:

SmallInt si = 3;	// 正确,SmallInt 构造函数不是 explicit 的
si + 3;				// 错误,此语句需要隐式转换,而类的运算符是 explicit 的
static_cast<int>(si) + 3;	// 正确,可以显式的进行转换。

​ 但是这里有一个例外,如果表达式被用作条件,则编译器会将显示的类型转换自动应用于它。

转换为 bool

​ 在 C++11 中,IO 标准库可以向 bool 进行显式的类型转换。

向 bool 的类型转换通常用在条件部分,因此 operator bool() const; 通常被定义为 explicit 的。

避免有二义性的类型转换

​ 如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。

​ 如,A 类中有构造函数接受 B 类,而 B 类中又定义了向 A 类进行类型转换的运算符。又或者,一个类中定义了多个与算术类型相关的转换规则。

实参匹配和相同的类型转换

​ 这里有 A 类和 B 类:

struct B;
struct A {
    A() = default;
    A(const B&);		// 将 B 转换成 A
    // 其他成员
};
struct B {
    operator A() const;		// 定义把 B 转换成 A 的类型转换运算符
};

当我们这样调用时:

A f(const A&);		// 声明一个形参为 const A& 的函数
B b;
A a = f(b);		// 二义性错误。含义是 f(B::operator A()) 还是 f(A::A(const B&))

​ 如果我们想指向上述调用,就必须显式的调用类型转换运算符或转换构造函数:

A a1 = f(b.operator A());		// ok
A a2 = f(A(b));					// ok

值得注意的是,我们无法使用强制类型转换来解决这个问题,因为强制转换本身也面临二义性。

二义性与转换目标为内置类型的多重类型转换

如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性问题。

​ 如下面代码:

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)

我们可以发现,在对 f2 的调用中,两种形式的类型转换运算符都不能精确的匹配 long double,所以产生二义性;当我们试图用 long 初始化 a2 时,也是同样的问题。

​ 上面两种情况之所以产生二义性,根本原因是它们所需的标准类型转换级别一致。所以如果我们的转换级别不一致,编译器将会按照转换级别选择最佳匹配:

short s = 42;
// 把 short 提升为 int 优先于把 short 转换为 double
A a3(s);		// 使用 A::A(int)
重载函数与转换构造函数

​ 当我们调用重载函数时,从多个类型转换中进行选择将变得更加复杂。如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。

​ 例如,当几个重载函数的参数分属不同的类类型时,如果这些类恰好定义了同样的转化构造函数,则二义性进一步提升:

struct C{
    C(int);
    // 其他成员
};
struct D {
    D(int);
    // 其他成员
};
void manip(const C&);
void manip(const D&);
manip(10);		// 二义性错误,含义是 manip(C(10)),还是 manip(D(10))

调用者可以显式地调用构造函数消除二义性。

重载函数与用户定义的类型转换

​ 当我们调用重载函数时,如果两个或多个用户定义的类型转换都提供了同一种可行匹配,则这些类型转换一样好。即使在这个过程中出现了标准类型转换的级别

​ 例如我们有类 E:

struct E{
    E(double);
    // 其他成员
};
void manip(const C&);
void manip(const E&);

manip2(10);		// 二义性错误,指 manip2(C(10)) 还是 manip(E(double(10)))

​ 上述 manip2 的调用仍发生二义性错误。即使其中一个调用需要额外的标准类型转换而另一个调用能精确匹配。

函数匹配与重载运算符

​ 重载运算符也是重载的函数。因此,通用的函数匹配规则同样适用于判断在给定的表达式中到底应该使用内置运算符还是重载运算符。

​ 例如,如果 a 是一种类类型,那么表达式 a sym b 可能是:

  • a.operatorsym(b); // a 有一个 operatorsym 成员函数
  • operatorsym(a,b); // operatorsym 是一个普通函数

和普通函数调用不同,我们不能通过调用的形式来区分当前调用的是成员函数还是非成员函数。

​ 当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数中。

当我们调用一个命名对象时,具有该名字的成员函数和非成员函数不会彼此重载。

当我们使用类类型的对象 (或者该对象的指针及引用) 进行函数调用时,只考虑该类的成员函数。


​ 举个栗子,我们为 SmallInt 定义一个加法运算符:

class SmallInt {
    friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
    SmallInt(int = 0);
    operator int() const { return val; }
private:
    std::size_t val;
};

可以使用这个类将两个 SmallInt 相加,但如果我们试图执行混合模式的算术运算,就将遇到二义性问题:

SmallInt s1, s2;
SmallInt s3 = s1 + s2;
int i = s3 + 0;		// 二义性错误

对于 int i = s3 + 0,我们可以将 0 转换为 SmallInt,也可以将 s3 转换为 int,发生二义性错误。

如果我们为同一个类既提供了转换目标是算术类型的类型转换,又提供了重载的运算符,将会遇到重载运算符与内置运算符的二义性问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值