《C++ Primer》筆記,整理關於函數重載與函數匹配的筆記。
函數重載
void func(int a); //原函數 void func(double a); //正確:形參類型不同 void func(int a, int b); // 正確:形參個數不同 int func(int a); //錯誤:只有返回類型不同 typedef int int32; void func(int32 a); //與原函數等價:形參類型相同 void func(const int a); //與原函數等價:頂層 const 將被忽略 void func(int); //與原函數等價:只是省略了形參名字
函數重載有如下的規則:
- 名字相同,形參類型不一樣。
- 不允許兩個函數除了返回類型外其他所有的要素都相同。
- 頂層
const的形參無法和沒有頂層const的形參區分。
其中返回類型不同時編譯時會出錯,而類型別名、項層const、省略形參名字只是重復聲明而已,只要不定義,編譯就不會出錯,比如:
//只定義了其中一個
void func(int a);
void func(const int a) {}
函數匹配
名字查找
函數匹配的第一步便是名字查找(name lookup),確定候選函數。
名字查找有兩方面:
- 常規查找(normal lookup)
- 實參決定的查找(argument-dependent lookup,ADL)
所有函數調用都會進行常規查找,只有函數的實參包括類類型對象或指向類類型對象的指針/引用的時候,才會進行實參決定的查找。
常規查找
void func(int a); //1
namespace N
{
//作用域
void func() {} //2
void func(double a) {} //3
...
void test1()
{
func(); //候選函數為函數2和3
}
void test2()
{
using ::func; //將函數1加入當前作用域
func(); //候選函數為函數1
}
...
}
從函數被調用的局部作用域開始,逐漸向上層尋找被調用的名字,一旦找到就停止向上尋找,將找到的所有名字加入候選函數。
此外,using語句可以將其他作用域的名字引用到當前作用域。
ADL查找
void func() {} //1
//第一個實參所在命名空間
namespace Name1 {
class T {
friend void func(T&) {} //2
};
void func(T) {} //3
}
//第二個實參的間接父類所在命名空間
namespace Name00 {
class T00 {
friend void func(int) {} //4
};
void func() {} //5
}
//第二個實參父類所在命名空間
namespace Name0 {
class T0:public Name00::T00 {
friend void func(int) {} //6
};
void func() {} //7
}
//第二個實參所在命名空間
namespace Name2 {
class T:public Name0::T0 {
friend void func(T&) {} //8
};
void func(T) {} //9
}
void test()
{
Name1::T t1;
Name2::T t2;
//9個函數全是候選函數
//第1個函數是normal lookup找到的
//后8個函數全是argument-dependent lookup找到的
func(&t1,t2);
}
從第一個類類型參數開始,依次遍歷所有類類型參數。對於每一個參數,進入其類型定義所在的作用域(類內友元函數也包括在內),並依次進入其基類、間接基類……定義所在的作用域,查找同名函數,並加入候選函數。
注意:在繼承體系中上升的過程中,不會因為找到同名函數就停止上升,這不同於常規查找。
類中的運算符重載也遵循 ADL 查找,其候選函數集既包括成員函數,也應該包括非成員函數。
namespace N
{
class A
{
public:
void operator+(int a) {} //1
};
void operator+(A &a, int a) {} //2
};
void operator+(A &a, int a) {} //3
void test()
{
N::A a;
a + 1; //1、2、3都是候選函數
}
確定可行函數
第二步便是從候選函數中選出可行函數,選擇的標准如下:
- 形參數量與本次調用提供的實參數量相等
- 每個實參的類型與對應的形參類型相同,或者能轉換成形參類型
//以下為候選函數
void func(int a, double b) {} //可行函數
void func(int a, int b) {} //可行函數:實參可轉化成形參類型
int func(int a, double b) {} //可行函數
void func(int a) {} //非可行函數:形參數量不匹配
void func(int a, int b[]) {} //非可行函數:實參不能轉換成形參
void test()
{
func(1, 0.1);
}
尋找最佳匹配
從可行函數中選擇最匹配的函數,如果有多個形參,則最佳匹配條件為:
- 該函數每個實參的匹配都不劣於其他可行函數需要的匹配。
- 至少有一個實參的匹配優於其他可行函數提供的匹配。
否則,發生二義性調用錯誤。
//可行函數
void func(int a, float b) {}
void func(int a, int b) {}
void test()
{
func(1, 0.1); //二義性錯誤:double 向 int 的轉換與向 float 的轉換一樣好
func(1, 1); //調用 void func(int a, int b)
}
為了確定最佳匹配,實參類型到形參類型的轉換等級如下:
- 精確匹配:
- 實參類型和形參類型相同。
- 實參從數組類型或函數類型轉換成對應的指針類型。
- 向實參添加頂層
const或者從實參中刪除頂層const。
- 通過
const轉換實現的匹配。 - 通過類型提升實現的匹配。
- 通過算術類型轉換或指針轉換實現的匹配。
- 通過類類型轉換實現的匹配。
一般不會存在這個階段不會同時存在兩個以上的精確匹配,因為兩個精確的匹配在本質上是等價的,在定義重載函數時,編譯器可能就報出重定義的錯誤了。
挑幾個重點的來詳細說一下。
指針轉換實現的匹配
- 0 或
nullptr能轉換成任意指針類型。 T *能轉換成void *,const void *轉換成const void*。- 派生類向基類類型的轉換。
- 函數與函數指針的形參類型必須精確匹配。
類類型轉換實現的匹配
兩個類型提供相同的類型轉換將產生二義性問題。
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::operator A()) 還是 f(A::A(const B&))
A a1 = f(b.operator A()); //正確:使用 B 的類型轉換運算
A a2 = f(A(b)); //正確:使用 A 的構造函數
類當中定義了多個參數都是算術類型的構造函數或類型轉換運算符,也會產生二義性問題。
struct A
{
A(int = 0);
A(double);
operator int() const;
operator double() const;
};
void f(long double);
A a;
f(a); //二義性錯誤:f(A::operator int()) 還是 f(A::operator double())?
long l;
A a2(l); //二義性錯誤:A::A(int) 還是 A::A(double)?
short s;
A a3(s); //正確:使用 A::A(int)
當我們使用兩個用戶定義的類型轉換時,如果轉換函數之前或之后存在標准類型轉換,則標准類型轉換將決定最佳匹配到底是哪個。
部分參考:http://particle128.com/posts/2013/11/name-lookup.html
原文地址:http://simpleyyt.github.io/2016/12/17/function-overload-and-match
