C++:28---类类型转换之类型转换运算符operator(explicit)

一、概念

类类型转换运算符是类的一种特殊成员函数,它负责将一个类转换为其他类型

  • 类定义了类类型转换函数之后,就可以隐式地与指定的operator类型进行操作

与类的隐式类型转换(转换构造函数)的不同:

  • 类的隐式类型转换:是将其他类型转换为类类型
  • 类型转换运算符:将类类型转换为其他类型

二、格式与注意事项

类型转换函数的形式:

  • type表示某种类型
  • 类型转换运算符可以面向任意类型(除了void)进行定义,只要该类型能作为函数的返回类型
operator type() const;

注意事项:

  • 不允许转换成数组或者函数类型,但允许转换为指针(包括数组指针以及函数指针)或者引用类型
  • 类型转换运算符没有显式的返回类型,也没有形参
  • 必须定义成类的成员函数
  • 类型转换函数通常应该为const类型
  • 并且转换的类型要与return结果类型相同
  • 类类型转换函数不能调用

错误格式的使用案例

  • 类型转换运算符是隐式执行的,所以无法给这些函数传递实参,当然也就不能在类型转换运算符的定义中使用任何形参
  • 同时,尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值

下面是一些错误的使用案例

class SmallInt {
public:
    int operator int() const;    //错误,不能指定返回类型
    operator int(int = 0)const;  //错误,不能有参数
    operator int*()const { return 42; }//错误,42与int*类型不一致
};

三、演示案例

下面的SmallInt类定义了两种类型的转换

  • 构造函数允许:将算术类型的值转换为SmallInt对象(这个就是我们介绍的类的隐式转换)
  • 类类型转换函数允许:将SmallInt对象转换为int
class SmallInt {
private:
    std::size_t val;
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; }
};

SmallInt si;
si = 4;  //首先将4隐式转换为SmallInt,然后调用SmallInt::operator=
cout << si + 3 << endl; //打印7。首先将si隐式地转换为int,然后执行整数的加法。如果不定义operator,那么这一步将出错

四、定义向bool类型的类型转换

  • 引入:在实际应用中,类很少提供类型转换运算符。在大多数情况下,如果类型转换自动发生,用户可能会感觉比较以外,而不是感觉受到了帮助。然后这条经验法则有一个例外:对于类来说,定义向bool的类型转换还是比较普遍的现象
  • 在C++的早期版本中,如果类想定义一个向bool的类型转换,则它常常遇到一个问题:因为bool是一种算术类型,所以类类型的对象转换为bool后就能被用在任何需要算术类型的上下文中。这样的类型转换可能引发意想不到的结果,特别是当istream含有向bool的类型转换时,下面的代码仍能编译通过:

演示案例 

int i=42;
cin <<i; //如果向bool的类型转换不是显式的,则该代码在编译器看来将是合法的
  • 这段程序试图将输出运算符作用域输入流。因为istream本身并没有定义<<,所以本来代码应该产生错误。然后,该代码能使用istream的bool类型转换运算符将cin转换为bool,而这个bool值接着会被提升成int并用作内置的左移运算符的左侧运算对象。这样一来,提升后的bool值(0或1)最终会被左移42个位置,这一结果显然与我们的与其大相径庭

C++11标准

  • 为了解决上面出现的问题,C++11标准中,IO标准库通过定义一个向bool的显示类型转换实现同样的目录
  • 无论我们什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。例如:
while(std::cin >> value)
  • while语句的条件执行输入运算符,它负责将数据读入到value并返回cin。为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换。如果cin的条件状态是good,则该函数返回为真;否则该函数返回为假

五、显式的类型转换运算符(explicit)

  • 为了防止四中的异常情况发生,C++11标准引入了显式的类型转换运算符explicit
  • 显示转换之后,不能直接使用隐式规则对类进行类型转换,需要使用static_cast显式的对类进行转换
  • 格式:通过explicit关键字定义
class SmallInt {
private:
    std::size_t val;
public:
    SmallInt(int i = 0) :val(i) {
        if (i < 0 || i>255)
            throw std::out_of_range("Bad SmallInt value");
    }
    explicit operator int()const { return val; }//编译器不会自动执行这一类型转换
};

SmallInt si;
si = 4;
cout << si+3 << endl; //错误,不能使用隐式的类型转换
cout << static_cast<int>(si)+3 << endl;//打印7,需要显示地请求类型转换

例外:

  • 如果表达式被用作条件,则编辑器会显示的类型转换自动应用于它、也就是说,当表达式出现在下列位置时,显示的类型转换被隐式地执行

六、避免有二义性的类型转换

  • 概念:如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则,我们编写的代码将很可能产生二义性

在两种情况下可能产生多重转换路径:

  • 第一种情况是两个类提供相同的类型转换:例如,当A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符,我们就说它们提供了相同的类型转换
  • 第二种情况是类定义了多个转换规则,而这些转换设计的类型本身可以通过其他类型转换联系在一起。最典型的例子是算术运算符,对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则

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

  • 下面的例子中,我们定义了两种将B转换成A的方法:一种使用B的类型转换运算符、另一种使用A的以B为参数的构造函数
//最好不要在两个类之间构建相同的类型转换
struct B;

struct A {
    A() = default;
    A(const B&); //把一个B转换成A
};

struct B {
    operator A()const; //也是把一个B转换成A
};

int main()
{
    A f(const A&);
    B b;
    A a = f(b);//二义性错误:含义是f(B::operator A())还是f(A::A(const B&))
    return 0;
}
  • 因为同时存在两种由B获得A的方法,所以造成编译器无法判断应该运行哪个类型转换,也就是说,对f的调用存在二义性

改正:

  • 如果我们确实想执行上述的调用,就必须显式地调用类型转换运算符或者转换构造函数
A f(const A&);
B b;
A a1 = f(b.operator A());  //正确,使用B的类型转换运算符
A a2 = f(A(b));  //正确,使用A的构造函数

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

  • 另外如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性的问题
  • 最简单也是最困扰我买的例子就是类当中定义了多个参数都是算术类型的构造函数,或者转换目标都是算术类型的类型转换运算符

例如,下面的类中包含两个转换构造函数,他们的参数是两种不同的算术类型;同时还包含两个类型转换运算符,它们的转换目标也恰好是两种不同的算术类型

struct A{
    A(int = 0);
    A(double);
    operator int()const;
    operator double() const;
};
void f2(long double);
int main()
{
    A a;
    f2(a);//二义性错误,含义是f(A::operator int())还是f(A::operator double())

    long lg;
    A a2(lg); //二义性错误:含义是A::A(int)还是A::A(double)
    return 0;
}
  • 对f2的调用中,哪个类型转换都无法精确匹配long double。然而这两个类型转换都可以使用,只要后面再执行一次生成long double的标准类型转换即可。因此,在上面的两个类型转换中哪个都不必另一个更好,调用将产生二义性

当我们试图用long初始化a2时也遇到了同样问题,哪个构造函数都无法精确匹配long类型。他们在使用构造函数前都要求先将实参进行类型转换:

  •  先执行long到double的标准类型转换,再执行A(double)
  •  先执行long到int的标准类型转换,再执行A(int)

编译器没办法区分这两种转换序列的好坏,因此调用产生二义性

  • 调用f2以及初始化a2的过程之所以产能生二义性,根本原因是他们所需的标准类型转换级别一致。当我们使用用户定义的类型转换时,如果转换过程包含标准类型转换,则标准类型转换的级别将决定编译器选择最佳匹配的过程:
short s=42;
//把short提升成int优先于把short转换为double
A a3(s);  //使用A::A(int)

在此案例中,把shor提升成int的操作要优先于吧short转换为double,因此编译器将使用A::A(int)构造函数构造a3,其中实参是s(提升后)的值 

重载函数与转换构造函数

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

重载运算符与类型转换运算符的二义性

  • 13
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值