《C++ Primer》之重载操作符与转换(下)
- 转换与类类型
可用一个实参调用的非 explicit 构造函数定义一个隐式转换。当提供了实参类型的对象而需要一个类类型的对象时,编译器将使用该转换。这种构造函数定义了到类类型的转换。除了定义到类类型的转换之外,我们还可以定义从类类型的转换。即,我们可以定义转换操作符,给定类类型的对象,该操作符将产生其他类型的对象。像其他转换一样,编译器将自动应用这个转换。在介绍如何定义这种转换之前,将说明它们为什么可能有用。
假定想要定义一个名为 SmallInt 的类,该类实现安全小整数,这个类将使我们能够定义对象以保存与 8 位 unsigned char 同样范围的值,即,0 到 255。这个类可以捕获下溢和上溢错误,因此使用起来比内置 unsigned char 更安全。我们希望这个类定义 unsigned char 支持的所有操作。具体而言,我们想定义 5 个算术操作符(+、-、*、/、%)及其对应的复合赋值操作符,4 个关系操作符(<、<=、>、>=),以及相等操作符(==、!=)。显然,需要定义 16 个操作符。而且,我们希望可以在混合模式表达式中使用这些操作符。例如,应该可以将两个 SmallInt 对象相加,也可以将任意算术类型加到 SmallInt。通过为每个操作符定义三个实例来达到目标:
int operator+(int, const SmallInt&);
int operator+(const SmallInt&, int);
SmallInt operator+(const SmallInt&, const SmallInt&);
因为存在从任意算术类型到 int 的转换,这三个函数可以涵盖支持 SmallInt 对象的混合模式使用的要求。但是,这个设计仅仅接近内置整数运算的行为,它不能适当处理浮点类型混合模式操作,也不能适当支持 long 、unsigned int 或 unsigned long 的加运算。问题在于这个设计将所有算术类型(甚至包括那些比 int 大的)转换为 int 并进行 int 加运算。即使忽略浮点或大整型操作数的问题,如果要实现这个设计,也必须定义 48 个操作符!幸好,C++ 提供了一种机制,利用这种机制,一个类可以定义自己的转换,应用于其类类型对象。对 SmallInt 而言,可以定义一个从 SmallInt 到 int 类型的转换。如果定义了该转换,则无须再定义任何算术、关系或相等操作符。给定到 int 的转换,SmallInt 对象可以用在任何可用 int 值的地方。//弊端:必须对加减乘除等所有操作符进行重载;必须对与整型、浮点型、double型等所有类型的操作都进行重载。
如果存在一个到 int 的转换,则以下代码:
SmallInt si(3);
si + 3.14159; // convert si to int, then convert to double
可这样确定:
将 si 转换为 int 值。将所得 int 结果转换为 double 值并与双精度字面值常量 3.14159 相加,得到 double 值。
- 转换操作符
转换操作符是一种特殊的类成员函数。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字 operator 之后跟着转换的目标类型:
class SmallInt {
public:
SmallInt(int i = 0): val(i)
{ if (i < 0 || i > 255)
throw std::out_of_range("Bad SmallInt initializer");
}
operator int() const { return val; }
private:
std::size_t val;
};
转换函数采用如下通用形式:
operator type();
这里,type 表示内置类型名、类类型名或由类型别名定义的名字。对任何可作为函数返回类型的类型(除了 void 之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的。转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。
虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。例如,operator int 返回一个 int 值;如果定义 operator Sales_item,它将返回一个 Sales_item 对象,诸如此类。转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const 成员。
- 使用类类型转换
只要存在转换,编译器将在可以使用内置转换的地方自动调用它:
在表达式中:
SmallInt si;
double dval;
si >= dval // si converted to int and then convert to double
在条件中:
if (si) // si converted to int and then convert to bool
将实参传给函数或从函数返回值:
int calc(int);
SmallInt si;
int i = calc(si); // convert si to int and call calc
作为重载操作符的操作数:
// convert si to int then call opeator<< on the int value
cout << si << endl;
在显式类型转换中:
int ival;
SmallInt si = 3.541; //
instruct compiler to cast si to int
ival = static_cast<int>(si) + 3;
使用转换函数时,被转换的类型不必与所需要的类型完全匹配。必要时可在类类型转换之后跟上标准转换以获得想要的类型。例如,在一个 SmallInt 对象与一个 double 值的比较中:
SmallInt si;
double dval;
si >= dval // si converted to int and then convert to double
- 只能应用一个类类型转换
类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。例如,假定有另一个类 Integral,它可以转换为 SmallInt 但不能转换为 int:
// class to hold unsigned integral values
class Integral {
public:
Integral(int i = 0): val(i) { }
operator SmallInt() const { return val % 256; }//已经被定义为转换为SmallInt了,就不能再转换为int了
private:
std::size_t val;
};
可以在需要 SmallInt 的地方使用 Integral,但不能在需要 int 的地方使用 Integeral:
int calc(int);
Integral intVal;
SmallInt si(intVal); // ok: convert intVal to SmallInt and copy to si
int i = calc(si); // ok: convert si to int and call calc
int j = calc(intVal); // error: no conversion to int from Integral
创建 si 时使用 SmallInt 复制构造函数。首先调用 Integral 转换操作符产生一个 SmallInt 类型的临时值,将 int_val 对象转换为 SmallInt。然后(合成的)复制构造函数使用该对象值初始化 si。第一个 calc 调用也是正确的:将实参 si 自动转换为 int,然后将 int 值传给函数。第二个 calc 调用是错误的:没有从 Integral 到 int 的直接转换。从 int 需要两次类类型转换:首先从 Integral 到 SmallInt,然后从 SmallInt 到 int。但是,语言只允许一次类类型转换,所以该调用出错。
标准转换可放在类类型转换之前。使用构造函数执行隐式转换的时候,构造函数的形参类型不必与所提供的类型完全匹配。例如,下面的代码调用 SmallInt(int) 类中定义的构造函数(SmallInt(int))将 sobj 转换为 SmallInt 类型:
void calc(SmallInt); short sobj; // sobj promoted from short to int // that int converted to SmallInt through the SmallInt(int) constructor calc(sobj);
如果需要,在调用构造函数执行类类型转换之前,可将一个标准转换序列应用于实参。为了调用函数 calc(),应用标准转换将 dobj 从 double 类型转换为 int 类型,然后调用构造函数 SmallInt(int) 将转换结果转换为 SmallInt 类型。
- 实参匹配和转换
类类型转换可能是实现和使用类的一个好处。通过为 SmallInt 定义到 int 的转换,能够更容易实现和使用 SmallInt 类。int 转换使 SmallInt 的用户能够对 SmallInt 对象使用所有算术和关系操作符,而且,用户可以安全编写将 SmallInt 和其他算术类型混合使用的表达式。定义一个转换操作符就能代替定义 48 个(或更多)重载操作符,类实现者的工作就简单多了。类类型转换也可能是编译时错误的一大来源。当从一个类型转换到另一类型有多种方式时,问题就出现了。如果有几个类类型转换可以使用,编译器必须决定对给定表达式使用哪一个。如果小心使用,类类型转换可以大大简化类代码和用户代码。如果使用得太过自由,类类型转换会产生令人迷惑的编译时错误,这些错误难以理解而且难以避免。
- 实参匹配和多个转换操作符
为了举例说明类类型值的转换怎样与函数匹配相互作用,我们给 SmallInt 类加上另外两个转换,包括接受一个 double 参数的构造函数和一个将 SmallInt 转换为 double 的转换操作符:
// unwise class definition: // multiple constructors and conversion operators to and from the built-in types // can lead to ambiguity problems class SmallInt { public: // conversions to SmallInt from int and double SmallInt(int = 0); SmallInt(double); // Conversions to int or double from SmallInt // Usually it is unwise to define conversions to multiple arithmetic types operator int() const { return val; } operator double() const { return val; } // ... private: std::size_t val; };
一般而言,给出一个类与两个内置类型之间的转换是不好的做法,在这里这样做是为了举例说明所包含的缺陷。//这个类既能和int双向转换,又能和double双向转换
考虑最简单的调用非重载函数的情况:
void compute(int); void fp_compute(double); void extended_compute(long double); SmallInt si; compute(si); // SmallInt::operator int() const fp_compute(si); // SmallInt::operator double() const extended_compute(si); // error: ambiguous
任一转换操作符都可用于 compute 调用中:operator int 产生对形参类型的完全匹配;//直接把si转换为int首先调用 operator double 进行转换,后跟从 double 到 int 的标准转换与形参类型匹配//先将si转变为double类型,再转变为int类型
完全匹配转换比需要标准转换的其他转换更好,因此,第一个转换序列更好,选择转换函数 SmallInt::operator int() 来转换实参。类似地,在第二个调用中,可用任一转换调用 fp_compute。但是,到 double 的转换是一个完全匹配,不需要额外的标准转换。最后一个对 extended_compute 的调用有二义性。可以使用任一转换函数,但每个都必须跟上一个标准转换来获得 long double//如果是将si转变为int,需要再将int转变为long double;如果将si转变为double,需要再将double转变为long double,因此,没有一个转换比其他的更好,调用具有二义性。如果两个转换操作符都可用在一个调用中//如extended_computer(si),既可以调用si的double转换,又可以调用si的int转换,而且在转换函数之后存在标准转换//需要将int转为long double,需要将double转换为long double,则根据该标准转换的类别选择最佳匹配。
运行:
class SmallInt { public: // conversions to SmallInt from int and double SmallInt(int = 0){}; SmallInt(double){}; // Conversions to int or double from SmallInt // Usually it is unwise to define conversions to multiple arithmetic types operator int() const { cout << "convert int" << endl; return val; } operator double() const { cout << "convert double" << endl; return val; } // ... private: std::size_t val; }; void compute(int){}; void fp_compute(double){}; void extended_compute(long double){}; int main() { SmallInt si; compute(si); // SmallInt::operator int() const fp_compute(si); // SmallInt::operator double() const extended_compute(si); // error: ambiguous system("pause"); return 0; }
报错:
- 实参匹配和构造函数转换
正如可能存在两个转换操作符,也可能存在两个构造函数可以用来将一个值转换为目标类型。考虑 manip 函数,它接受一个 SmallInt 类型的实参:
void manip(const SmallInt &); double d; int i; long l; manip(d); // ok: use SmallInt(double) to convert the argument manip(i); // ok: use SmallInt(int) to convert the argument manip(l); // error: ambiguous
在第一个调用中,可以用任一构造函数将 d 转换为 SmallInt 类型的值。int 构造函数需要对 d 的标准转换,而 double 构造函数完全匹配。因为完全匹配比标准转换更好,所以用构造函数 SmallInt(double) 进行转换。
在第二个调用中,情况恰恰相反,构造函数 SmallInt(int) 提供完全匹配——不需要附加的转换,调用接受一个 double 参数的 SmallInt 构造函数需要首先将 i 转换为 double 类型。对于这个调用,用 int 构造函数转换实参。
第三个调用具有二义性。没有构造函数完全匹配于 long。使用每一个构造函数之前都需要对实参进行转换:标准转换(从 long 到 double)后跟 SmallInt(double);标准转换(从 long 到 int)后跟 SmallInt(int)。这些转换序列是不能区别的,所以该调用具有二义性。当两个构造函数定义的转换都可以使用时,如果存在构造函数实参所需的标准转换,就用该标准转换的类型选择最佳匹配。
运行如下代码:
class SmallInt { public: // conversions to SmallInt from int and double SmallInt(int = 0){ cout << "construct int" << endl; }; SmallInt(double){ cout << "construct double" << endl; }; // Conversions to int or double from SmallInt // Usually it is unwise to define conversions to multiple arithmetic types operator int() const { cout << "convert int" << endl; return val; } operator double() const { cout << "convert double" << endl; return val; } // ... private: std::size_t val; }; void compute(int){}; void fp_compute(double){}; void extended_compute(long double){}; void manip(const SmallInt &){}; int main() { double d = 0.0; int i = 0; long double l = 0; manip(d); manip(i); system("pause"); return 0; }
运行结果:
加一行:
报错:
- 当两个类定义了转换时的二义性
当两个类定义了相互转换时,很可能存在二义性:
class Integral; class SmallInt { public: SmallInt(Integral); // convert from Integral to SmallInt // ... }; class Integral { public: operator SmallInt() const; // convert from SmallInt to Integral // ... }; void compute(SmallInt); Integral int_val; compute(int_val); // error: ambiguous
实参 int_val 可以用两种不同方式转换为 SmallInt 对象,//compute(int_val);需要将int_val转变为SmallInt类型这里,可以试用SmallInt类的构造函数SmallInt(Integral)将int_val转变为SmallInt类型,也可以试用Integral类的转换操作符将int_val转换为SmallInt类型。因为这两个函数没有高下之分,所以这个调用会出错。//Integral到SmallInt类的转换有两条“路径”,所以但凡涉及到这两个类型的转换就会出现二义性
在这种情况下,不能用显式类型转换来解决二义性——显式类型转换本身既可以使用转换操作又可以使用构造函数,相反,需要显式调用转换操作符或构造函数:
compute(int_val.operator SmallInt()); // ok: use conversion operator compute(SmallInt(int_val)); // ok: use SmallInt constructor
而且,由于某些似乎微不足道的原因,我们认为可能有二义性的转换是合法的。例如,SmallInt 类构造函数复制它的 Integral 实参,如果改变构造函数以接受 const Integral 引用:
class SmallInt { public: SmallInt(const Integral&);//这里改为引用 };
则对 compute(int_val) 的调用不再有二义性!原因在于使用 SmallInt 构造函数需要将一个引用绑定到 int_val,而使用 Integral 类的转换操作符可以避免这个额外的步骤。这一小小区别足以使我们倾向于使用转换操作符。避免二义性最好的方法是避免编写互相提供隐式转换的成对的类。
- 避免转换函数的过度使用
与使用重载操作符一样,转换操作符的适当使用可以大大简化类设计者的工作并使得类的使用更简单。但是,有两个潜在的缺陷:定义太多转换操作符可能导致二义性代码,一些转换可能利大于弊。
避免二义性最好的方法是,保证最多只有一种途径将一个类型转换为另一类型。做到这点,最好的办法是限制转换操作符的数目,尤其是,到一种内置类型应该只有一个转换。
当转换操作符用于没有明显映射关系的类类型和转换类型之间时,容易引起误解,在这种情况下,提供转换函数可能会令类的使用者迷惑不解。例如,如果有一个表示 Date 的类,我们可能会认为提供从 Date 到 int 的转换是个好主意,但是,这个转换函数应返回什么值?该函数可以返回公历日期,这是表示当前日期的一个顺序数,以 0 表示 1 月 1 日,但年份是否应放在日期之前或之后?即,1986 年 1 月 31 日是否应表示为 1986031 或 311986?作为一种选择,转换操作符可以返回一个表示从某个新纪元点开始计数的天数,计数器可以从 1971 年 1 月 1 是或其他起始点开始计算天数。问题在于,无论怎样选择,Date 对象的使用将具有二义性,因为没有一个 Date 类型对象与 int 类型值之间的一对一映射。在这种情况下,不定义转换函数更好。相反,这个类应该定义一个或多个普通成员从这些不同形式中抽取信息。
- 重载确定和类的实参
正如我们看到的,在需要转换函数的实参时,编译器自动应用类的转换操作符或构造函数。因此,应该在函数确定期间考虑类转换操作符。函数重载确定由三步组成:确定候选函数集合:这些是与被调用函数同名的函数;选择可行的函数:这些是形参数目和类型与函数调用中的实参相匹配的候选函数。选择可行函数时,如果有转换操作,编译器还要确定需要哪个转换操作来匹配每个形参;选择最佳匹配的函数。为了确定最佳匹配,对将实参转换为对应形参所需的类型转换进行分类。对于类类型的实参和形参,可能的转换的集合包括类类型转换。
- 转换操作符之后的标准转换
哪个函数是最佳匹配,可能依赖于在匹配不同函数中是否涉及了一个或多个类类型转换。如果重载集中的两个函数可以用同一转换函数匹配,则使用在转换之后或之前的标准转换序列的等级来确定哪个函数具有最佳匹配。否则,如果可以使用不同转换操作,则认为这两个转换是一样好的匹配,不管可能需要或不需要的标准转换的等级如何。//没理解往下看
void compute(int); void compute(double); void compute(long double);
假定使用原来的 SmallInt 类,该类只定义了一个转换操作符——从 SmallInt 到 int 的转换,那么,如果将 SmallInt 对象传给 compute,该调用与接受一个 int 的 compute 版本相匹配。
三个函数都是可行的:
compute(int) 可行,因为 SmallInt 有到 int 的转换,该转换是对形参的完全匹配;compute(double) 和 compute(long double) 也是可行的,可以使用到 int 的转换,后面跟上适当的用于 double 或 long double 的标准转换。因为可以用同一类类型转换来匹配这三个函数,如果存在标准转换,就用标准转换的等级确定最佳匹配。因为完全匹配比标准转换更好,所以选择 compute(int) 函数作为最佳可行函数。只有两个转换序列使用同一转换操作时,才用类类型转换之后的标准转换序列作为选择标准。//如,不论是调用compute(int),compute(double)还是compute(long double),都是得先使用“SmallInt转换为int”这个转换,所以这三者才能比较出优劣;倘若,有从SmallInt转换为int的转换,又有从SmallInt转换为double的转换,就比较不出compute(int)和compute(double)的优劣了。
- 多个转换和重载确定
现在可以看看为什么增加一个到 double 的转换是个坏主意。如果使用修改后的定义了到 int 和 double 的转换的 SmallInt 类,则用 SmallInt 值调用 compute 具有二义性:
class SmallInt { public: // Conversions to int or double from SmallInt // Usually it is unwise to define conversions to multiple arithmetic types 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
在这个例子中,可以使用 operator int 转换 si 并调用接受 int 参数的 compute 版本,或者,可以使用 operator double 转换 si 并调用 compute(double)。编译器将不会试图区别两个不同的类类型转换。具体而言,即使一个调用需要在类类型转换之后跟一个标准转换,而另一个是完全匹配,编译器仍会将该调用标记为错误。//这种情况下,当调用compute(SmallInt),compute就不知道是该将SmallInt转换为int从而调用compute(int)还是将SmallInt转换为double调用compute(double)了。
- 显示强制转换消除二义性
面对二义性转换,程序员可以使用强制转换来显式指定应用哪个转换操作:
void compute(int); void compute(double); SmallInt si; compute(static_cast<int>(si)); // ok: convert and call compute(int)
这个调用现在是合法的,因为它显式指出了将哪个转换操作应用到实参。实参类型强制转换为 int,该类型与接受 int 参数的第一个 compute 版本完全匹配。
- 标准转换和构造函数
比如:
class SmallInt { public: SmallInt(int = 0); }; class Integral { public: Integral(int = 0); }; void manip(const Integral&); void manip(const SmallInt&); manip(10); // error: ambiguous
问题在于,Integral 和 SmallInt 这两个类都提供接受 int 参数的构造函数,其中任意一个构造函数都可以与 manip 的一个版本相匹配,因此,函数调用有二义性:它既可以表示将 Integral 转换为 int 并调用 manip 的第一个版本,也可以表示 SmallInt 转换为 int 并调用 manip 的第二个版本。
即使其中一个类定义了实参需要标准转换的构造函数,这个函数调用也可能具有二义性。例如,如果 SmallInt 定义了一个构造函数,接受 short 而不是 int 参数,函数调用 manip(10) 将在使用构造函数之前需要一个从 int 到 short 的标准转换。在函数调用的重载版本中进行选择时,一个调用需要标准转换而另一个不需要,这一事实不是实质性,编译器不会更喜欢直接构造函数,调用仍具有二义性。//Why?
- 显示构造函数调用消除二义性
调用者可以通过显式构造所需类型的值而消除二义性:
manip(SmallInt(10)); // ok: call manip(SmallInt) manip(Integral(10)); // ok: call manip(Integral)
在调用重载函数时,需要使用构造函数或强制类型转换来转换实参,这是设计拙劣的表现。
- 重载、转换和操作符
重载操作符就是重载函数,使用与确定重载函数调用一样的过程来确定将哪个操作符(内置的还是类类型的)应用于给定表达式。给定如下代码:
ClassX sc; int iobj = sc + 3;
有四种可能性:
有一个重载的加操作符与 ClassX 和 int 相匹配;存在转换,将 sc 和/或 int 值转换为定义了 + 的类型。如果是这样,该表达式将先使用转换,接着应用适当的加操作符;因为既定义了转换操作符又定义了 + 的重载版本,该表达式具有二义性;因为既没有转换又没有重载的 + 可以使用,该表达式非法。
- 重载确定和操作符
成员函数和非成员函数都是可能的,这一事实改变了选择候选函数集的方式。
- 操作符的候选函数
一般而言,候选函数集由所有与被使用的函数同名的函数构成,被使用的函数可以从函数调用处看到。对于操作符用在表达式中的情况,候选函数包括操作符的内置版本以及该操作符的普通非成员版本。另外,如果左操作符具有类类型,而且该类定义了该操作符的重载版本,则候选集将包含操作符的重载版本。一般而言,函数调用的候选集只包括成员函数或非成员函数,不会两者都包括。而确定操作符的使用时,操作符的非成员和成员版本可能都是候选者。
确定指定函数的调用时,与操作符的使用相反,由调用本身确定所考虑的名字的作用域。如果是通过类类型的对象(或通过这种对象的引用或指针)的调用,则只需考虑该类的成员函数。具有同一名字的成员函数和非成员函数不会相互重载。使用重载操作符时,调用本身不会告诉我们与使用的操作符函数作用域相关的任何事情,因此,成员和非成员版本都必须考虑。
- 警告:转换和操作符
正确设计类的重载操作符、转换构造函数和转换函数需要多加小心。尤其是,如果类既定义了转换操作符又定义了重载操作符,容易产生二义性。下面几条经验规则会有所帮助:
不要定义相互转换的类,即如果类 Foo 具有接受类 Bar 的对象的构造函数,不要再为类 Bar 定义到类型 Foo 的转换操作符。
避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型的对象,然后可以使用内置操作符。不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。
最简单的规则是:对于那些“明显正确”的,应避免定义转换函数并限制非显式构造函数。
- 转换可能引起内置操作的二义性
我们再次扩展 SmallInt 类。这一次,除了到 int 的转换操作符和接受 int 参数的构造函数(类和内置类型转换的两种方式)之外,将增加一个重载的加操作符:
class SmallInt { public: SmallInt(int = 0); // convert from int to SmallInt // conversion to int from SmallInt operator int() const { return val; } // arithmetic operators friend SmallInt operator+(const SmallInt&, const SmallInt&); private: std::size_t val; };
现在,可以用这个类将两个 SmallInts 对象相加,但是,如果试图进行混合模式运算,将会遇到二义性问题:
SmallInt s1, s2; SmallInt s3 = s1 + s2; // ok: uses overloaded operator+ int i = s3 + 0; // error: ambiguous
第一个加使用接受两个 SmallInt 值的 + 的重载版本。第二个加有二义性,问题在于,可以将 0 转换为 SmallInt 并使用 + 的 SmallInt 版本//先利用构造函数SmallInt(int = 0)将0转换为SmallInt类型,再利用重载的加号操作符实现,也可以将 s3 转换为 int 值并使用 int 值上的内置加操作符//利用转换操作符int()实现。
- 可行的操作符函数和转换
通过为每个调用列出可行函数,可以理解这两个调用的行为。在第一个调用中,有两个可行的加操作符:
operator+(const SmallInt&, const SmallInt&)
The built-in operator+(int, int),即内置的 operator+(int, int)。
第一个加不需要实参转换——s1 和 s2 与形参的类型完全匹配。使用内置加操作符对两个实参都需要转换,因此,重载操作符与两个实参匹配得较好,所以将调用它。对于第二个加运算:
int i = s3 + 0; // error: ambiguous
两个函数同样可行。在这种情况下,重载的 + 版本与第一个实参完全匹配,而内置版本与第二个实参完全匹配。第一个可行函数对左操作数而言较好,而第二个可行函数对右操作数而言较好。因为找不到最佳可行函数,所以将该调用标记为有二义性的。//第一个可行函数会利用SmallInt(int)构造函数将0转换为SmallInt类型,然后再加;第二个内置的operator+会利用转换操作符将SmallInt转换为int类型,再进行加。