基本概念
重载的运算符本质上是具有特殊名字的函数,名字由关键字operator和后面要定义的运算符号共同组成。该函数也包含返回类型、参数列表、函数体。
重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元有一个参数,二元有两个参数,lhs第一个rhs第二个……
除了重载函数调用运算符
operator()
之外,其他重载运算符不能含有默认实参。如果运算符是类的成员函数,则第一个(左侧)运算对象绑定到隐式的this指针上。因此需要的参数数量总是少一个。
对于一个运算符函数,其或者是类的成员,或者至少含有一个类类型:内置类型一般都已经有自己重载的运算符。
只能重载已有的而不能发明新的运算符号。
有四个符号(+、-、*、&)可以作为一元或二元,视重载参数量而定。
重载的运算符,优先级和结合律与对应内置运算符一致。
//可被重载的运算符
+ - * / % ^
& | ~ ! , =
< > += -= *= /= %= ^= &= |=
<< >> >>= <<= == != <= >= <=>(since C++20)
&& || ++ -- ->* -> () []
new new[] delete delete[]
//不能重载为友元的运算符
= () [] ->
//不能重载的运算符
:: .* . ?: sizeof
直接调用一个重载的运算符函数
data1 + data2;
//等价于
operator+(data1, data2);
data1 += data2;
//等价于
data1.operator+=(data2);
不应被重载的运算符
重载的运算符本质上是一次函数调用,因此关于运算对象求值顺序的规则无法应用到重载的运算符中。尤其是逻辑与、逻辑或、逗号的运算对象求值顺序规则无法保留下来。此外,&&和||的重载版本无法保留内置的短路求值属性,两边都会被求值。因此不建议重载
一般不重载逗号和取地址的原因:C++中这两种运算符用于类类型都有特殊含义,因此在使用上不建议修改。
使用与内置类型一致的含义
首先考虑设计的类将提供的操作,再思考操作应该被设为普通函数还是重载运算符。如果某些擦欧总在逻辑上与运算符相关则应定义为重载运算符。
- 类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致;
- 类检查相等性,则定义operator==,往往和operator!=成对出现;
- 类包含内在单序比较,则定义operator<。常常与其他关系操作operator>、operator>=、operator<=一起出现。
- 重载运算符的返回类型通常应该与其内置版本的返回类型兼容:
-
- 逻辑和关系运算符返回bool
-
- 算数运算符返回类类型的值
-
- 赋值运算符和复合赋值运算符返回lhs的一个引用。
赋值和复合赋值运算符
是否将运算符重载作为类的成员,有如下准则:
- 赋值
=
下标[]
调用()
成员访问->
必须是成员 - 复合赋值一般是成员,但不是必须
- 改变对象状态的或者与给定类型密切相关的运算符,如递增递减解引用,通常应该是成员
- 具有对称性的运算符可以转换任意一端的对象,如算数、相等性、关系、位运算,应该是普通的非成员函数。
输入和输出运算符
IO库使用<<
和>>
执行输入和输出。类需要自定义适合其的新版本来支持IO操作。
重载输出运算符<<
通常情况下:
- 第一个形参是一个非常量ostream对象的引用,非常量是因为写入流需要改变其状态;引用是因为无法直接复制一个ostream对象。
- 第二个形参是一个常量的引用,常常是需要输出的类类型。第二个形参是引用是为了避免拷贝实参;可以是常量是因为打印对象不会改变对象。
- 返回的一般是ostream的形参,为了与其他输出运算符保持一致。
输出运算符尽量减少格式化操作
减少换行符等的存在,使用户容易控制输出的细节。
一般而言,输出运算符只负责打印对象内容,而非控制格式。
输出运算符必须是非成员函数
因为lhs的参数不可能是类类型的对象。
IO运算符通常被声明为友元来访问类的非公有成员。
重载输入运算符>>
通常情况下:
- 第一个形参是运算符将要读取的流的引用。可以是常量。
- 第二个形参是将要读入到的非常量对象的引用。是非常量是因为本质上就是把数据读到这个对象中。
- 返回某个给定流的引用(一般是lhs)。
istream& operator>>(istream &is, Foo& item){
int val;
is>>Foo.a>>Foo.b>>val;
if(is) //输入成功
Foo.c = Foo.a * val;
relse
item = Foo(); //对象赋予默认状态
return is; //返回流的引用
}
输入时的错误
执行输入运算符时可能的错误:
- 当流含有错误类型的数据时
- 当读取操作到达文件末尾或遇到输入流的其他错误时
读取发生错误时,输入运算符应该负责从错误中恢复。
标示错误
有些输入运算符会做更多数据验证,确保数据合乎规范。通常输入运算符只设置failbit。
应该由IO标准库自己表示的错误:设置eofbit表示文件耗尽。而设置badbit表示流被破坏。
算数和关系运算符
- 通常把这两种运算符定义为非成员函数来允许左右对象进行转换。
- 由于不需要改变对象状态,形参一般都是常量的引用。
- 该运算符通常计算两个对象并得到新值,该值一般位于局部变量内,并将其返回。
算术运算符
如果类中定义了算术运算符,则一般会定义一个对应的复合赋值运算符。最有效的方式是使用复合赋值定义算术运算符:
Foo operator+(const Foo& lhs, const Foo& rhs){
Foo sum = lhs;
sum += rhs;
return sum;
}
相等运算符
通常,只有当对应成员都相等时,才认为两个对象相等。设计准则如下:
- 如果一个类含有判断来个那个对象是否相等的操作,则显然应该定义为
operator==
而非普通函数。 - 如果类定义了
operator==
,则该运算符应该能判断一组给定对象中是否含有重复数据 - 相等运算符应该具有传递性
- 如果类定义了
operator==
,则也应该定义operator!=
,反之亦然,并将一个用另一个的取反来定义。
关系运算符
关联容器和一些算法用到operator<
更多,所以定义小于更有用。
通常情况下,关系运算符应该:
- 定义顺序关系,使其与关联容器中对关键字的要求一致
- 如果类中同时有
operator==
,则定义一种关系使其与相等运算符一致。尤其是!=的两个对象一定一大一小。 - 若类中不存在逻辑可靠的
<
定义,则不必重载该运算符。
赋值运算符
可以把类的一个对象赋值给该类的另一个对象。也可以定义其他赋值运算符来使用的类型作为右侧运算对象。返回值一般为该类类型的一个引用。
还可以为initializer_list定义赋值运算符,该类运算符由于形参类型不同,不必定义自赋值条件:
Foo& operator=(std::initializer_list il){
auto data = alloc_n_copy(il.begin(), il.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
复合赋值运算符
一般把复合赋值运算符在内的所有赋值运算符定义在类的内部。类中的复合赋值运算符也要返回其左侧对象的引用。
下标运算符
下标运算符必须是成员函数。,通常以所访问的元素的引用作为返回值,好处是下标可以出现在赋值运算的任意一端。
最好同时定义下标运算符的常量版本(同时是类的常量成员)和非常量版本
递增和递减运算符
递增递减使得类可以在元素的序列中前后移动。并不要求递增递减运算符是类的成员,但由于改变的正好是操作对象的状态,建议设为成员函数。
对于内置类型,递增和递减运算符有前置和后置版本。应该为类定义两个版本的递增和递减运算符。
前置递增递减运算符
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
对于指针,需要存在判断语句判断是否可以继续前移或后移。
区分前置和后置运算符
前置和后置运算符的重载形式完全相同,区分方法是:后置版本接受一个不被使用的int类型的形参,编译器会自动为其提供一个值为0的实参。通常而言这个形参唯一的作用就是区分前置和后置。
Foo& operator=(); //前置
Foo& operator=(int); //后置
后置运算符一般通过调用各自的前置版本完成工作,例如执行
Foo ret = *this;
++*this
return ret;
先由前置运算符判断是否安全再执行对象的递增,但是返回的依然是之前保存到临时变量的ret的值。
显式调用后置运算符
添加int实参0即可。
Foo f;
f.operator++(0); //后置版本
f.operator++(); //前置版本
成员访问运算符
解引用首先检查对象是否在作用范围内,如不可在尾后位置。如果在,则返回所指元素的一个引用。
定义了箭头运算符的类通常具有解引用运算符,且一般将其实际工作委托给解引用运算符。
对箭头运算符返回值的限定
重载箭头运算符时,可以改变箭头从哪个对象获取成员,但箭头获取成员这一事实不能改变。
对于形如point->mem的表达式,point必须是指向类对象的指针或者一个重载了operator->类的对象。根据point类型的不同,point->mem分别等价于:
(*point).mem; //point使一个内置的指针类型
//?
point.operator()->mem; //point是类的一个对象
除此之外的代码都会发生错误。实际上,point->mem
的执行过程为:
- 如果point是指针:应使用内置的箭头运算符,表达式等价于
(*point).mem
。首先解引用该指针,然后从所得对象中获取指定成员。如果point所指的类型没有名为mem的成员,程序发生错误。 - 如果point是定义了
operator->
类的一个对象,则使用point.operator->()
的结果来获取mem。- 其中,如果该结果是一个指针,则执行第一步(上条中的步骤);
- 如果该结果本身含有重载的
operator->()
,则重复当前步骤。 - 直到最终,这一过程结束时程序返回所需内容,或返回错误信息。
重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
函数调用运算符
函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间类似函数重载,应该在参数上有所区别。
如果类重载了函数调用运算符,则可以像使用函数一样使用该类的对象。这样的类同时可以储存状态,因此比普通函数更灵活:
下例类的对象可以返回一个绝对值,这个过程十分类似函数调用:
struct AbsInt{
int operator()(int val) const {
return val<0 ? -val : val;
}
};
int main(){
int a = -100;
AbsInt ai;
auto res = ai(a);
}
如果类定义了调用运算符,则该类的对象称作函数对象(function object),因为可以调用这种对象:“行为像函数一样”
含有状态的函数对象类
函数对象类除了operator()之外也可以包含其他成员。函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符中的操作:
class PrintStr{
public:
PrintStr(ostream& o = cout, char c = ' '):
os(o), sep(c) {}
void operator()(const string&s) const {
os<
该类对象在初始化时可以确定输出流和输出时插入的间隔。而这些都是由成员对象作为状态指示的。
lambda就是函数对象
含有lambda的函数表达式可以改写为类的对象形式。以下两个stable_sort
效果完全一样:
#include
#include
#include
#include
using std::string;
using std::vector;
class StbSort{
public:
bool operator()(const string& s1, const string& s2) const{
return s1.size() words = {"how", "hi", "hello", "hall"};
auto class_sort = StbSort();
std::stable_sort(words.begin(), words.end(),
[](const string& s1, const string& s2){
return s1.size() < s2.size();
});
for (auto i: words) std::cout<
类的调用运算符的形参列表和函数体与lambda表达式的完全一样。
默认情况下lambda不能改变其捕获的变量。因此,默认情况下lambda产生的类当中函数调用运算符是一个const成员函数。除非lambda被声明为可变的。
表示lambda及相应捕获行为的类
一个lambda通过引用捕获变量时应确保执行期间该变量的确存在。因此编译器可以直接使用该引用而无需在lambda产生的类中将其作为数据成员。
通过值捕获的变量被拷贝到lambda中。因此这种lambda产生的类应该为每个值捕获的变量建立对应数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。
而创建lambda自身的数据成员之后则其已经与所捕获的变量无关。这一事实则可以解释10.3中的以下现象:
也就是说,第一次捕获变量就会初始化lambda内部的成员值,后续即使改变了被捕获变量本身的值也再也不会初始化lambda中相应甚至同名的成员变量。
void chglbd(){
int x = 1;
int y = 2;
auto f = [&, x]()mutable{ //x为传值捕获
x += y; //若无mutable,x无法被赋值
return x;
};
std::cout<
lambda表达式产生的类不含默认构造函数、赋值运算符以及默认析构函数。它是否含有默认拷贝/移动构造函数则通常视要捕获的数据成员的类型而定。
标准库定义的函数对象
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。例如
-
plus
类定义了一个函数调用运算符对于一对运算对象执行+
的操作; -
modulus
类定义了一个调用运算符执行二元的%
操作; -
equal_to
类执行==
-
negate
类执行取反操作等。
这些类都定义在
头文件中。
这些类都被定义为了模板的形式,因此可以为其指定具体的应用类型。该处的类型即调用运算符的形参类型。如plus
令string加法运算符作用于string对象等等。
#include
int main(){
std::plus pls_int;
auto res_pls = pls_int(int(10), int(20));
std::negate ngt_int;
auto res_ngt = ngt_int(int(10));
std::equal_to eql_int;
auto res_eql = eql_int(int(10), int(20));
std::modulus mdl_int;
auto res_mdl = mdl_int(int(5), int(2));
std::cout<
标准库函数对象
//算数
plus
minus
mulitplies
divides
modulus
neagate
//关系
equal_to
not_equal_to
greater
greater_equal
less
less_equal
//逻辑
logical_and
logical_or
logical_not
算法中使用标准库函数对象
可以用来替换算法中的默认运算符或者谓词:
std::vector v(10);
int cnt = 0;
for (auto& i: v) i += ++cnt;
//实现从大到小的排列
std::sort(v.begin(), v.end(), std::greater);
标注奴规定某些函数对象对于指针同样适用。因此可以使用这些函数对象对指针使用标准库算法。
可调用对象与function
C++中的可调用对象:
- 函数
- 函数指针
- lambda表达式
- bind创建的对象
- 重载了函数调用运算符的类
可调用的对象具有其类型;函数及函数指针的类型由其返回值和实参决定。
两个不同的可调用对象可以共享同一种 调用形式(call signature) 。
调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型:
int(int, int)
是一个函数类型,接受两个int并返回一个int。
不同类型可能具有相同调用形式
几个可调用对象共享同一种调用形式的情况,有时会把它们看成相同的类型:
int add(int i, int j){return i + j;}
auto mod = [](int i, int j){return i % j;}
struct divide{
int operator()(int i, int j){
return i / j;
}
}
尽管上述可调用对象的实现方式不同,但他们具有相同的调用形式:
int(int, int)
如果要使用这些可调用对象构建一个简单的计算器,需要定义一个 函数表(function table) 用于储存指向这些可调用对象的“指针”。当程序需要执行某个特定的操作时,从表中查找该调用的函数。
C++中,可以使用map实现函数表。假设所有函数都相互独立,并且只处理关于int的二元运算,则map可以定义为:
map binops;
并按照如下形式将add的指针添加到binops中:
binops.insert({"+", add});
由于上述mod和divide可调用对象并非函数指针,不能将其添加到表中。二者有自己的类型,且该类型与map不匹配。
解决方法:标准库类型function
标准库function类型
function类型定义在头文件
中。其定义的操作有:
function | - |
---|---|
function |
f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同。即,T是retType(args)。 |
function |
显式构造一个空function |
function |
在f中存储可调用对象obj的副本 |
f |
将f作为条件:当f含有一个可调用对象时为真;否则假。 |
f(args) |
调用f中的对象,参数是args |
function |
- |
---|---|
result_type |
该funciton类型可调用对象返回的类型 |
argument_type |
当T有一个或两个实参时定义的类型。只有一个实参,则是该类型的同义词;有两个实参,则下列成员 |
first_argument_type |
代表第一个实参的类型 |
second_argument_type |
代表第二个实参的类型 |
function是一个模板,和其他模板一样,在创建时需要提供额外的信息。
此例中的额外信息指该funciton类型可表示的对象的调用形式:
function
则,结合上述的divide、add和mod,一个简易的计算器如下:
struct divide{
int operator()(const int& lhs, const int& rhs){
return lhs / rhs;
}
};
auto mod = [](const int& lhs, const int& rhs){
return lhs % rhs;
};
int add(const int& lhs, const int& rhs){
return lhs + rhs;
}
int main(){
//类可调用对象只能通过拷贝初始化,否则对象销毁
std::function f_dvd = divide();
std::function f_mod(mod);
std::function f_add(add);
std::multiplies f_mul; //兼容functional中定义的函数对象
std::map> f_map;
//注意:为map插入的是pair,应使用初始化列表,直接拷贝需要嵌套大括号
f_map.insert({"/", f_dvd});
f_map.insert({"%", f_mod});
f_map.insert({"+", f_add});
f_map.insert({"-", [](const int& lhs, const int& rhs){return lhs - rhs;}});
f_map.insert({"*", f_mul});
int a = 8, b = 5;
std::map res;
for (auto i: f_map){
int res_cur = f_map[i.first](a, b);
std::cout<
重载的函数与function
不能直接将重载函数的名字存入function类型的对象中,因为通过名字无法确认到底是哪个重载了的函数被装入。
int add(const int& a, const int& b){return a + b;};
Foo add(const Foo& a, const Foo& b){return a + b;};
解决上述二义性的方式是:储存函数指针而非函数的名字:
auto (*add1)(int, int) = add;
auto (*add2)(Foo, Foo) = add;
func_map.insert({"+", add1});
或者使用lambda,避免使用重载函数,从而消除二义性。
新版标准库的function类与旧版本中的unary_functon和binary_function没有关联。后两个类已经被更通用的bind函数代替了。
重载、类型转换、运算符
由一个实参调用的非显式构造函数定义了一种隐式的类型转换。这种构造函数将实参类型的对象转换为类类型。
同样能定义对于类类型的类型转换,通过定义类型转换运算符可以做到。
转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversions) 这样的转换有时称为用户定义的类型转换(user-defined conversions)
类型转换运算符
类型转换运算符 conversion operation 是类的一种特殊成员函数。它负责将一个类类型的值转换为其他类型。类型转换函数的一般形式如下:
operator type() const;
其中const表示某种类型。类型转换运算符可以面向任意类型(除了void)进行定义,只要该类型能作为函数的返回类型。因此,不允许转换成数组或函数类型,但允许转换成指针(数组指针和函数指针)或引用类型。
类型转换运算符既没有显式的返回类型也没有形参,而且必须定义成类的成员函数。一般被定义成const成员。
定义含有类型转换运算符的类
class SmallInt{
std::size_t val;
public:
SmallInt(int i = 0) : val(i){ //构造函数负责将int转为类类型对象
if(i < 0 || i > 255) throw std::out_of_range("bad value");
}
operator int() const {return val;} //类型转换运算符负责将类对象转换成int
};
int main(){
int i(8);
SmallInt si(i);
auto r = si + 3; //可以如此隐式转换
std::cout<
编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。因此,可以将任何算术类型传递给该类类型的构造函数。类似地,能使用类型转换运算符将一个类类型对象转换为运算符定义的算术类型,再通过该算术类型转换为任何其他的算术类型。
类型转换运算符是隐式执行的,所以无法给这些函数传递实参,当然也就不能在定义中使用任何形参。同时,尽管类型转换函数不指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值。
避免过度使用类型转换函数。尤其是类类型的对象的数据和算术类型不存在明确对应关系,如时间到int等。
类型转换运算符可能产生意外结果
尤其是类定义一个向bool类型的类型转换,在早期的c++中,转换后的类类型对象能被用在任何需要算术类型的上下文中。因此常常发生意想不到的结果,尤其是istream含有向bool的类型转换,因此下列代码会编译通过:
int i = 42;
cin << i; //该操作将cin隐式转换为bool
则该操作最后的结果将是cin转换为bool,进而可以提升为int,最终左移42个位置!
显式的类型转换运算符
为了避免以上情况,C++ 11引入了显式的类型转换运算符(explicit conversion operator),使得编译器不会自动进行该类型的转换,只是使该类型支持此种转换:
class SmallInt{
std::size_t val;
public:
SmallInt(int i = 0) : val(i){
if(i < 0 || i > 255) throw std::out_of_range("bad value");
}
explicit operator int() const {return val;}
};
则可以进行显式转换,比如使用static_cast
:
int main(){
int i(8);
SmallInt si(i);
auto r1 = int(si) + i;
std::cout<(si) + i;
std::cout<
该规定存在一个例外:如果表达式被用作条件,则编译器会直接将显式的类型转换自动应用于它,也就是说,下列位置的表达式,即使是显式的类型转换也会被隐式执行:
-
if
、while
以及do
语句的条件部分 -
for
语句头的条件表达式 - 逻辑非
!
、逻辑或||
、逻辑与&&
的运算对象 - 条件运算符
? :
的条件表达式
转换为bool
标准库的早期版本,IO类型定义了向 void*
的转换规则以求避免隐式的bool转换,但新版本的解决方案已经改为定义一个向bool的显式类型转换。
无论什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool
,例如:
while(std::cin>>value)
在该条件中,cin被istream的operator bool
类型转换函数隐式执行了转换:
- 若cin的条件状态为good,则bool转换函数为真
- 否则为假。
避免二义性的类型转换
如果类中包含一个或多个类型转换,则必须确保在类类型和布标类型之间只存在唯一一种转换方式。否则会产生二义性代码。
两种情况会产生多种转换路径:
- 第一种:两个类提供相同的类型转换。
-
- 例如:A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符
- 第二种:类定义了多个转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系在一起。
-
- 例如:算术运算符,对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则。
实参匹配和相同的类型转换
上述第一种引起多种转换路径的情况:
struct B;
struct A{
A() = default;
A(const B&); //B到A的构造转换
};
struct B{
B() = default;
operator A() const; //B到A的类型转换运算符
};
int main(){
A f(const A&);
A a;
B b;
A a1 = A(b); //通过构造转换,正确
A a2(b); //通过构造转换,正确
A a3 = static_cast(b); //正确
A ax1 = f(b); //二义性错误
A ax2 = b; //二义性错误
}
解决上述问题的方法是,显式使用其中一种转换方式:
A a4 = f(b.operator A());
A a5 = f(A(b));
A a6 = f(static_cast(b));
二义性与转换木雕为内置类型的多重类型转换
如果类定义了一组类型转换,它们的转换原或转换目标类型本身可以通过其他类型联系在一起,则同样会产生二义性问题。
最简单却最困扰的:类当中定义了多个参数都是算术类型的构造函数,或者转换目标都是算术类型的类型转换运算符。
例如:
struct A{
A(int = 0);
A(double);
operator int() const;
operator double() const;
};
上述类除了int和double之外无法精确匹配的类型转换则会产生二义性。
根本原因:所需的标准类型转换级别一致,无法找到最佳匹配。
定义了多个类型转换,如果转函数之前或之后存在标准类型转换,则标准类型转换决定最佳匹配。
经验规则:
- 不要令两个类执行相同的类型转换,尤其是A由B构造和B转换到A。
- 避免转换目标是内置算术类型的类型转换。特别是已经定义了一个时。解决方法:
-
- 不要定义接受算术类型的重载运算符。如果用户需要该运算符,则类型转换操作将转换类型对象,然后使用内置运算符。
-
- 不要定义转换到多种算术类型的类型转换,让标准类型转换完成不同算术类型之间的转换。
重载函数与转换构造函数
调用重载的函数时,如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。
如果调用重载函数时需要使用构造函数或者强制类型转换改变实参类型,则说明程序存在不足。
重载函数与用户定义的类型转换
调用重载函数时,如果两个或多个用户定义类型转换都提供了可行匹配,则认为这些转换一样好。
该过程中,不考虑任何可能出现的标准类型转换的级别。只有当重载函数嫩通过一个类型转换函数得到匹配时,才考虑其中出现的标准类型转换。
在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义类型转换不止一个,则具有二义性。
函数匹配与重载运算符
重载的运算符也是重载的函数,因此上述二义性情况同样适用。此外,重载运算符的候选会比重载函数更多,因为需要考虑的有:
a.operator+(b);
operator+(a, b);
两种情况。难以通过调用的形式区分到底是不是成员函数版。
当使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。
而调用一个命名的函数时,具有该名字的成员函数和非成员函数不会彼此重载。因为用来调用命名函数的语法形式对于成员和非成员是不同的。
如果对一个类提供了转换目标是算术类型的类型转换,又提供了重载的运算符,则可能遇到重载运算符和内置运算符二义性问题。因为不知道是进行类类型运算还是转换为算术类型进行内置运算。