首页 > 代码库 > C++ Primer 学习笔记_64_重载操作符与转换 --转换与类类型【下】

C++ Primer 学习笔记_64_重载操作符与转换 --转换与类类型【下】

重载操作符与转换

--转换与类类型【下】

四、重载确定和类的实参

在需要转换函数的实参,编译器自动应用类的转换操作符或构造函数。因此,应该在函数确定期间考虑类转换操作符。函数重载确定由三步组成:

1)确定候选函数集合:这些是与被调用函数同名的函数。

2)选择可行的函数:这些是形参数目和类型与函数调用中的实参相匹配的候选函数。选择可行函数时,如果有转换操作,编译器还要确定需要哪个转换操作来匹配每个形参

3)选择最佳匹配的函数。为了确定最佳匹配,对将实参转换为对应形参所需的类型转换进行分类。对于类类型的实参和形参,可能的转换的集合包括类类型转换


1、转换操作符之后的标准转换

如果重载集中两个函数可以用同一转换函数匹配,则使用在转换之后或之前的标准转换序列的等级来确定哪个函数具有最佳匹配。

否则,如果可以使用不同转换操作,则认为这两个转换是一样好的匹配,不管可能需要或不需要的标准转换的等级如何。

注意:只有两个转换序列使用同一转换操作时,才用类类型转换之后的标准转换序列作为选择标准


2多个转换和重载确定

class SmallInt
{
public:
    operator int() const
    {
        return val;
    }

    operator double() const
    {
        return val;
    }
    //...

private:
    std::size_t val;
};

    void compute(int);
    void compute(double);
    void compute(long double);

    SmallInt si;
    compute(si);    //Error:ambiguous

在这个例子中,可以使用operatorint 转换si并调用接受int参数的compute版本,或者,可以使用operatordouble 转换si并调用compute(double)

编译器将不会试图区别两个不同的类类型转换。具体而言,即使一个调用需要在类类型转换之后跟一个标准转换,而另一个是完全匹配,编译器仍会将该调用标记为错误。即:

    void compute(int);
    //void compute(double);
    void compute(long double);

    SmallInt si;
    compute(si);    //Error:ambiguous,此处仍然错误

3、显式强制转换消除二义性

    void compute(int);
    void compute(double);
    void compute(long double);

    SmallInt si;
    compute(static_cast<int>(si));  //OK
    compute(static_cast<double>(si));   //OK

4、标准转换和构造函数

class SmallInt
{
public:
    SmallInt(int = 0);
};
class Integral
{
public:
    Integral(int = 0);
};

    void manip(const SmallInt &);
    void manip(const Integral &);
    /*
    *:它既可以将 Integral 转换为 int 并调用 manip 的第一个版本,
    *也可以表示 SmallInt 转换为 int 并调用 manip 的第二个版本。
    */
    manip(10);  //Error

即使其中一个类定义了实参需要标准转换的构造函数,这个函数调用也可能具有二义性。例如,如果SmallInt定义了一个构造函数,接受short而不是 int参数,函数调用manip(10)将在使用构造函数之前需要一个从intshort的标准转换。在函数调用的重载版本中进行选择时,一个调用需要标准转换而另一个不需要,这一事实不是实质性,编译器不会更喜欢直接构造函数,调用仍具有二义性。如:

class SmallInt
{
public:
    //将int该为short
    SmallInt(short = 0);
};
class Integral
{
public:
    Integral(int = 0);
};

    void manip(const SmallInt &);
    void manip(const Integral &);

    //此处调用仍具有二义性
    manip(10);  //Error

5、显式构造函数调用消除二义性

    manip(SmallInt(10));    //OK
    manip(Integral(10));    //OK

【警告!】

在调用重载函数时,需要使用构造函数或强制类型转换实参,这是设计拙劣的表现!

//P463 习题14.44
class LongDouble
{
public:
    operator double () const
    {
        cout << "double!" << endl;
        return val;
    }
    operator float () const
    {
        cout << "float!" << endl;
        return val;
    }

private:
    double val;
};

int main()
{
    LongDouble ldObj;
    int ex1 = ldObj;    //Error
    float ex2 = ldObj;  //OK : float
}

//习题14.45
class LongDouble
{
public:
    LongDouble(double);

private:
    double val;
};

void calc(int temp)
{
    cout << "int" << endl;
}
void calc(LongDouble temp)
{
    cout << "LongDouble" << endl;
}

int main()
{
    double dval;
    //将会调用void calc(int):因为标准转换要优于类类型转换
    calc(dval);
}

五、重载、转换和操作符

重载操作符就是重载函数。使用与确定重载函数调用一样的过程来确定将哪个操作符(内置的还是类类型的)应用于给定表达式。

    ClassX sc;
    int iobj = sc + 3;

有四种可能性:

1)有一个重载的加操作符与ClassXint相匹配。

2)存在转换,sc/int值转换为定义了+的类型。如果是这样,该表达式将先使用转换,接着应用适当的加操作符。

3)因为既定义了转换操作符又定义了+的重载版本,该表达式具有二义性

4)因为既没有转换又没有重载的+可以使用,该表达式非法。


1、重载确定和操作符

成员函数和非成员函数都是可能的,这一事实改变了选择候选函数集的方式!

操作符的重载确定遵循常见的三步过程(老生常谈了:-D):

1)选择候选函数

2)选择可行函数,包括识别每个实参的潜在转换序列

3)选择最佳匹配的函数


2、操作符的候选函数

一般而言,候选函数集由所有与被使用的函数同名的函数构成,被使用的函数可以从函数调用处看到。对于操作符用在表达式中的情况,候选函数包括操作符的内置版本以及该操作符的普通非成员版本。另外,如果左操作符具有类类型,而且该类定义了该操作符的重载版本,则候选集将包含操作符的重载版本

一般而言,函数调用的候选集只包括成员函数或非成员函数,不会两者都包括。而确定操作符的使用时,操作符的非成员和成员版本可能都是候选者

确定指定函数的调用时,与操作符的使用相反,调用本身确定所考虑的名字的作用域。如果是通过类类型的对象(或通过这种对象的引用或指针)的调用,则只需考虑该类的成员函数。具有同一名字的成员函数和非成员函数不会相互重载。使用重载操作符是时,调用本身不会告诉我们与使用的操作符函数作用域相关的任何事情,因此,成员和非成员版本都必须考虑。

【警告:转换和操作符】

正确设计类的重载操作符转换构造函数转换函数需要多加小心。尤其是,如果类既定义转换操作符又定义了重载操作符,容易产生二义性。下面几条经验规则会有所帮助:

1)不要定义相互转换的类,如果类Foo具有接受类Bar的对象的构造函数,不要再为类Bar定义到类型Foo的转换操作符

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

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

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

最简单的规则是:对于那些“明显正确”的,避免定义转换函数限制非显式构造函数


3、转换可能引起内置操作符的二义性

class SmallInt
{
public:
    SmallInt(int = 0);
    operator int() const
    {
        return val;
    }

    friend SmallInt operator+(const SmallInt &,const SmallInt &);
private:
    std::size_t val;
};

int main()
{
    SmallInt s1,s2;

    //使用接受两个 SmallInt 值的 + 的重载版本
    SmallInt s3 = s1 + s2;  //OK

    //以将 0 转换为 SmallInt 并使用 + 的 SmallInt 版本
    //也可以将 s3 转换为 int 值并使用 int 值上的内置加操作符。
    int i = s3 + 0; //Error
}


【小心地雷】

既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性。


4可行的操作符和转换

通过为每个调用列出可行函数,可以理解这两个调用的行为。在第一个调用中,有两个可行的加操作符:

    SmallInt operator+(const SmallInt &,const SmallInt &);
    内置的 operator+(int, int)

第一个加不需要实参转换—— s1s2与形参的类型完全匹配。使用内置加操作符对两个实参都需要转换,因此,重载操作符与两个实参匹配得较好,所以将调用它。

对于第二个加运算:

    int i = s3 + 0; // error: ambiguous

两个函数同样可行。在这种情况下,重载的+版本与第一个实参完全匹配,而内置版本与第二个实参完全匹配。第一个可行函数对左操作数而言较好,而第二个可行函数对右操作数而言较好。因为找不到最佳可行函数,所以将该调用标记为有二义性的。

//P466 习题14.46 最后的一条语句会调用哪个operator+
class Complex
{
public:
    Complex(double);
};

class LongDouble
{
    friend LongDouble operator+(LongDouble &,int);  //1

public:
    LongDouble(int);
    operator double ();
    LongDouble operator+(const Complex &);      //2
    //..
};
LongDouble operator+(const LongDouble &,double);    //3

int main()
{
    LongDouble ld(16.08);
    double res = ld + 15.05;    //调用3,为什么呢?
}