重载函数与二义性问题

该文章为《C++ Prime》 第5版读书笔记,以下内容全部来自《C++ Prime》 第5版的读书笔记整理。笔者为c++初学者,不能保证本文所述内容全部正确,请读者仔细甄别。如果您发现了任何问题,欢迎使用电子邮箱与我联系:Kelin92024@163.com


函数重载

在同一作用域内的几个函数名字相同但形参列表不同,称之为重载函数。如下所示:

1
2
void print(const char *p);
void print(const int *p, const int *q);

注意:不允许两个函数除了返回类型外其它所有要素都相同。

特别的,对于const,有如下规则:

1
2
3
4
5
6
7
8
9
10
11
Record lookup(Phone);
Record lookup(const Phone); //error, 重复声明了Record lookup(phone)

Record lookup(phone*);
Record lookup(phone* const); //error, 重复声明了Record lookup(Phone*)

Record lookup(Account&);
Record lookup(const Account&); //正确

Record lookup(Account*);
Record lookup(const Account*); //正确

即:顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

函数匹配

函数匹配也叫做重载确定,是指把函数调用与一组重载函数中的某一个关联起来的过程。函数匹配遵循以下步骤:

  1. 选定本次调用的重载函数集,集合中的函数称为候选函数,候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可用。

  2. 从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参与本次调用提供的实参数量相等,二是每个实参的类型与对于的形参类型相同,或者能转换成形参的类型。

  3. 从可行函数中寻找最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功。

    • 该函数每个实参的匹配都不劣与其他可行函数的匹配
    • 至少有一个实参的匹配优于其他可行函数提供的匹配

    如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的,编译器将报告二义性调用的信息。

举例说明:

1
2
3
4
void f(int, int);
void f(double, double);

f(42,2.56);//显然,该调用具有二义性

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分为几个等级,具体排序如下所示:

  1. 精确匹配,包括
    • 实参类型与形参类型相同
    • 实参从数组类型或函数类型转换成对于的指针类型
    • 向实参添加顶层const或者从实参删除顶层const
  2. 通过const转换实现的匹配,将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。
  3. 通过类型提升实现的匹配
  4. 通过算数类型转换或指针转换实现的匹配(指针转换包括:0或nullptr转换成任意指针类型,指向任意非常量的指针转换void*,指向任意对象的指针转换成const void* 等)
  5. 通过类类型转换实现的匹配。

重载、类型转换与运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct B;

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

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

A f(const A&);
B b;
A a = f(b); //二义性错误

如上所示,当调用f(b),编译器无法分辨是调用f(B::operator A())还是f(A::A(const B&)),如果我们向确实执行上述的调用,应当显式的调用类型转换运算符或转换构造函数:

1
2
A a1 = f(b.operator A());
A a2 = f(A(b));

类似的,有如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct A {
A(int = 0); //最好不要创建两个转换源都是算数类型的类型转换
A(double);
operator int() const; //最好不要创建两个转换对象都是算数类型的类型转换
operator double() const;
}

void f2(long double);
A a;
f2(a); //二义性错误

long lg;
A a2(lg); //二义性错误

在对f2的调用中,哪个类型转换都无法精确匹配long double,编译器无法判断是调用f2(A::operator int())还是f2(A::operator double()),在使用long初始化a2时,编译器也无法判断调用A::A(int)还是A::A(double)

当几个重载函数的参数分属于不用的类类型时,如果这些类恰好定义了同样的转换构造函数,则二义性问题将进一步提升,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct C {
c(int);
//...
};
struct D {
D(int);
};

void manip(const C&);
void manip(const D&);

manip(10); //二义性错误,mainip(C(10))或是mainip(D(10))
//应显式调用,如:
manip(C(10));

当调用重载函数,如果多个用户定义的类型转换都提供了可行匹配,那么编译器不会考虑任何可能出现的标准类型转换的级别,只有当重载函数能通过同一个类型转换函数得到匹配时,才会考虑其中出现的标准类型转换,例如:

1
2
3
4
5
6
7
8
9
10
11
12
struct C {
c(int);
//...
};
struct D {
D(double);
};

void manip2(const C&);
void manip2(const D&);

manip2(10); //二义性错误,两个不用用户定义的类型转换都能用在此处

尽管D的构造函数需要对实参进行标准类型转换,但这次调用仍然具有二义性错误。

重载的运算符也是重载的函数,因此,通用的函数匹配规则同样适用于在给的的表达式中应当使用内置运算符还是重载运算符。

1
2
3
4
5
6
7
8
9
10
11
12
class SmallInt {
friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0);
operator int() const {return val;}
private:
std::size_t val;
}

SmallInt s1, s2;
SmallInt s3 = s1 + s2;//调用重载的operator+
int i = s3 + 0; //二义性错误 可以把0转换成SmallInt,然后调用重载的operator+,也可以把s3转换成int,对两个int执行内置的加法运算。

在编程中需要避免上述问题,尤其是类同时定义了类型转换运算符及重载运算符时。尽量遵循以下规则:

  • 不要令两个类执行相同的类型转换,当A类可以通过B类对象构造时,不要在B类中定义转换目标时A类的类型转换运算符
  • 避免转换目标是内置算术类型的类型转换,如果已经定义了一个,则不要再定义接受算数类型的重载运算符,也不要定义转换到多种算数类型的类型转换。