转换与类类型

来源:互联网 发布:linux系统安全 编辑:程序博客网 时间:2024/05/16 12:04

转换与类类型

        今天看到<<C++Primer>>第14章9小节的转换与类类型,给我的感触是:类类型转换确实强大,但是却极易出错,很容易产生难以察觉的二义性,如非功力深厚,切记慎之。

        此文不打算讨论类类型转换问题,因为本人实在没怎么掌握,仅以此记住该书有此一难点,希望以后有幸深入探讨。

        虽然不讨论过于复杂的类类型转换的二义性问题及规避方法,但还是把手中的忠告罗列一下:

(1) 不要定义相互转换的类,如果类A中具有接受类B的对象的构造函数,不要再为类B定义到类型A的转换,即一种转换不要多种实现。

(2) 避免到内置算术类型的转换。具体而言,如果定义了到算数类型的转换,则:

1)   不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型对象,然后可以使用内置操作符。

2)   不要定义转换到一个以上的算术类型的转换。让标准转换提供到其他算术类型的转换。

总之:仅重载一个到内置类型的类型转换函数,其他转换交给标准转换;尽量不要实现类与类之间的转换;如果实现了类与类直接的转换最好不要重载算术操作符;在必要的地方进行显示类型转换总是一个绝好的选择。

 

在书中464页最后一段有这么一句描述:对于普通的成员函数,它是使用类的对象显示调用,或类的作用域显示调用,所以不会与非成员函数构成重载,即使名字参数完全相同;但是,对于操作符重载,调用本身不会告诉我们与使用的操作符函数作用域相关的任何事情,因此,成员操作符和非成员操作符版本能够构成重载。

以上描述貌似非常明确,其实不然,看看下面几个测试程序你就会明白了:

测试一:

#include<iostream>using std::cout;using std::endl;class Inter{public:    Inter(){}    Inter(int v):val(v){}    Inter& operator=(const Inter& r)    {         val = r.val;         return *this;    }    Inter operator+(const Inter& d)    {         cout<<"inner"<<endl;         return Inter(val+d.val);    }    int getval()const{return val;}private:    int val;}; Inter operator+(const Inter& opa,const Inter& opb){    cout<<"outer"<<endl;    return Inter(opa.getval()+opb.getval());} int main(){      Inter add = Inter(10)+Inter(20);  }

结果有点出乎意料,调用结果输出:inner,奇怪,难道primer上说错了?按理说不是构成重载了吗?难道这两个不会出现调用的不明确吗?仔细思考一下觉得应该不是书上的问题。

 

想想只有一种可能,那就是内部的operator+的默认操作数(第一个操作数)不是const 引用,遂改之如下:

Inter operator+(const Inter&opa,const Inter& opb)

改为:

Inter operator+(Inter&opa,const Inter& opb)

结果仍然输出:inner,怎么搞得,进一步思考,那就是成员操作符operator+的第一个操作符默认是标量而非引用。

再次改为:

Inter operator+(Inter opa,const Inter&opb)

这次如我们所愿,出现调用时出现ambiguous情形,说明调用具有二义性,但由于两个函数作用域不同,所以编译期不会出现重定义,且由于“a+b“形式的调用不能告诉编译器有关operator+作用域的任何信息,那么它会去全局以及相关类中查找,结果发现两个版本都可以使用且无法区分哪个是最佳匹配,由此产生二义性。


那为什么两次都优先调用类的函数呢? 一个可能的解释:优先调用,否则解释不通。否则引用和标量的重载在调用时会出现二义性。但在后面的一个例子中我们可以看出其标量和引用都会造成二义性,这无法解释,按理说默认的this指针应该是type *const this。不可能会是同一个版本,这里笔者也搞不懂,只能说我们决不要在类内实现了操作符重载还傻呼呼的在外面画蛇添足,这样会使得程序极难维护。(此两处我们只能在一处对操作符进行重载),在调用“A+B”时A,B应该都当作标量处理。但第一个默认参数类型还真不知道。

一个较为合理的解释: A+B处理时,编译器把操作数当作标量处理,而A++,++A则把操作数当作引用处理(这非常合呼情理)。还有一个性质是标量和其引用版本构成重载,但是调用时却发生二义性,这是为什么后面的例子除了引用类型调用不成功外,标量也不行。那为什么“+“的引用没问题呢,这就是另一个特点,特别的对于操作符重载,若多于两个参数则标量及其引用可构成重载,且调用不会发生二义性。


其实此时也不是没有解决方案,我们只需显示调用operator+函数即可:

修改代码如下:

#include<iostream>using std::cout;using std::endl;class Inter{public:    Inter(){}    Inter(int v):val(v){}    Inter& operator=(const Inter& r)    {         val = r.val;         return *this;    }    Inter operator+(const Inter& d)    {         cout<<"inner"<<endl;         return Inter(val+d.val);    }    int getval()const{return val;}private:    int val;}; Inter operator+(Inter opa,const Inter& opb){    cout<<"outer"<<endl;    return Inter(opa.getval()+opb.getval());}int main(){      //Inter add = Inter(10)+Inter(20);    Inter add = Inter(10).operator+(Inter(20));    cout<<add.getval()<<endl;    Inter add2 = operator+(Inter(10),Inter(20));    cout<<add2.getval()<<endl;}

 

这样就能正常调用了,道理也非常简单:

Inter(10).operator+(Inter(20))

通过对象调用函数operator+为其指明了函数所在的类作用域,这样就不会出现二义性。而对于后者:

Inter add2 =operator+(Inter(10),Inter(20))

这个也是,把operator+作为函数调用,且参数个数也不同,自然是不会出现二义性。

 

我们需要特别注意操作符可以向使用普通函数一样使用这个特性。

 

其实以上我们还是有一个有一个问题不够明确

Inter add2 =operator+(Inter(10),Inter(20))

的调用到底是由于作为函数调用还是因为参数个数不同而导致二义性的消失呢?()

 

解释这个问题之前我们先来实现一个有意思的功能:在类外的非成员函数中实现前置++和后置++操作符尽管C++中不主张这么做,我们仅仅是为了了解语言本身而为之。基本公式如下:

(1) 前置:type& operator++(type&);

(2) 后置:type operator++(type&,int);

看如下代码:

#include<iostream>using std::cout;using std::endl; class Inter{public:    Inter(){}    Inter(int v):val(v){}    Inter& operator=(const Inter& r)    {       val = r.val;       return *this;    }       //Inter& operator++();    //Inter operator++(int);    int getval()const{return val;}    int& reference(){return val;}private:    int val;}; Inter& operator++(Inter& r){    ++r.reference();    return r;} Inter operator++(Inter& r,int){    Inter tmp = r;    ++r;    return tmp;} int main(){    Inter add(30);    cout<<(++add).getval()<<endl;    cout<<add++.getval()<<endl;}

好了这就实现了上述功能了!

下面进一步测试:

#include<iostream>using std::cout;using std::endl; class Inter{public:    Inter(){}    Inter(int v):val(v){}    Inter& operator=(const Inter& r)    {       val = r.val;       return *this;    }    Inter& operator++()    {       ++val;       return *this;    }    Inter operator++(int)    {       Inter tmp = *this;       val++;       return tmp;    }    int getval()const{return val;}    int& reference(){return val;}private:    int val;}; Inter& operator++(Inter& r){    r.reference() += 1;    return r;} Inter operator++(Inter& r,int){    Inter tmp = r;    r.reference() += 1;    return tmp;} (4)/*Inter& operator++(){    return Inter(100);}*/  int main(){    Inter add(30);    (1)//cout<<(++add).getval()<<endl;    (2)//cout<<add++.getval()<<endl;    cout<<add.operator++().getval()<<endl;    cout<<add.operator++(0).getval()<<endl;    cout<<operator++(add).getval()<<endl;    cout<<operator++(add,0).getval()<<endl;    (3)//cout<<operator++()<<endl;}

(1)(2)的调用会产生二义性在前面已经解释过,后面就是函数式调用。明显不会出现二义性,至少作用域不同,参数个数也不同。后来企图用(3)测试是否是参数个数导致无二义性,但是失败了,原因在于(4)编译提示++操作符在类外定义至少需要一个类类型或者枚举类型的参数。

 

这么说来无法知道我们问题的答案了,那就先搁着把。

目测是作为函数调用其查找方式是按作用域的,二者不可能冲突,而作为操作符定义会有一些特殊的查找方式。

 

操作符查找规则(匹配规则):C++中重载操作符有两种调用规则,一种就是把operator操作符当作普通函数使用,其匹配规则与普通函数无异;另一种用操作符特有的方式调用,如A+B,这样的话没有编译器任何关于操作符的作用域信息,此时编译器会同时在当前作用域及参数所在类域中查找

 

 

 

 

原创粉丝点击