C++中的默認參數規則
C++的默認參數規則其實是一個非常容易掉坑的規則,尤其是當一個函數擁有多個聲明的時候,每個聲明的默認參數可以各不相同,在調用時又可能與每個聲明都不同;這篇博客稍微列舉一下C++中的默認參數規則。
前置
在開始之前,我們先來復習一下,函數可以有多個聲明,定義 是有函數體的聲明。默認參數則是函數聲明中使用特殊語法(decl-specifier-seq declarator = initializer
)為某個參數提供的默認值。這種語法就像在參數列表中寫參數對象的拷貝初始化一樣。
void foo(int a, int b = 0);
void func(int a = 1, int b = 2, int c = 3);
默認參數語法存在的目的,是為用戶在函數調用時,可以不提供尾隨參數。
func();
func(1);
func(1,2);
非常容易理解,由於C++目前還沒有像python那樣指定參數的語法,因而需要提供默認值的參數必須放到參數列表的后面。實質上,這相當於編譯器替用戶向函數中傳遞參數。
那么,這里就有這么幾個問題,什么地方可以有默認參數?哪些東西可以作為默認參數?當函數有多個聲明時,默認參數如何工作?
什么地方可以有默認參數?
到我開始寫這篇文章為止,C++允許普通的函數聲明(包括類成員函數)、lambda表達式中使用默認參數,而不允許函數指針、函數引用以及typedef聲明中出現默認參數。具體可見這里。所以,在需要管理各種包裝計算函數的對象的場景中(algorithm factory),由於大部分手法使用函數指針進行,保存默認參數需要使用額外的空間並在運行時完成。
這里可以稍微留個問題,std::function支持默認參數嗎?如果不,為什么?
什么可以作為默認參數?
從文法上說,所有能做initializer的東西都能作為默認參數,但問題在於,有些東西出現在initializer中是不被允許的,這個范圍還挺廣。
- 局部變量不能在默認參數中
- this指針不能在默認參數中
- 其他的參數不能在默認參數中
- 非靜態成員不能在默認參數中
- 有捕獲內容的lambda表達式不能在默認參數中
int main(int argc = 0,char** argc = {0}); // ok
int foo(int a1, int a2, int a3 = a1 + a2); // bad
int func(int a1, int a2, int a3 = 1 + 2); // ok
void helper()
{
int n = 1;
int func(int a1 = n);//bad
}
//C is a class with copy constructor
C::foo(C p = *this); // bad
class C
{
static int s;
int a;
//void func(int n = a);//bad
void func(int n = s);//OK
}
//after C++11
int globalV;
void defaultFuncs(int a1 = ([]()->int {return 0; })(), int a2 = 1, int a3 = 3 + 2);//ok
//void defaultFuncs(int a1 = ([]()->int {return globalV; })(), int a2 = 1, int a3 = 2);//bad
並且,由於默認參數實質上就是編譯器替用戶填參數,而函數調用時會發生argument到parameter的拷貝初始化,因而這個initializer必須要能夠滿足到相應parameter的拷貝初始化。
除此之外,其它的東西都可以做默認參數。
多個聲明與默認參數
如果每個函數都只有一個聲明兼定義,那么事情會簡單很多,但是,在C++中,一個函數可以有多個聲明,但只能有一個定義,這和名稱查找規則共同協作,構成了C++的分離編譯特性。隨之而來的,默認參數在多個聲明之間有很有趣的工作特性。
多個聲明之間的默認參數組合
第一個特性,就是不同聲明之間的默認參數是能組合的。
void multi(int a1, int a2, int a3)
{
std::cout << a1 << a2 << a3 << '\n';
}
void multi(int a1,int a2, int a3 = 3);
void multi(int a1, int a2 = 2, int a3);
void multi(int a1 = 1, int a2, int a3);
int main()
{
multi();//ok ,call multi(1,2,3);
}
這是一個很隱蔽的特性,隱蔽到筆者的VS2017intelliScene會對它報錯,然而編譯還是能通過。事實上,標准中有這么一句規定:
所有的有默認參數的形參后面,所有的形參都必須在這個聲明或者在先前的聲明提供默認參數,或者是參數包。
在函數調用點,實際上的默認參數是函數所有可見聲明的默認參數的聯合。但是相應的,在這個可見集合中,不能有對於同一個形參重復的默認參數聲明,即使是同一個值也不行。
int foo(int,int);
int foo(int a1, int a2 = 0);
int foo(int a1, int a2 = 0);//bad
內部作用域
簡單而言,內部作用域中可以重新聲明一個函數,並且可以忽視外部聲明的默認參數。在這個內部作用域中的函數調用,其默認參數集合也是這個這個作用域中所有聲明的默認參數聯合。當然,這僅僅是可見聲明這個概念,也就是名稱查找的小游戲而已。
同理,如果你外部作用域與內部作用域均有默認參數定義,那么using會同時把默認參數導入進來。
其它的小規則
默認參數還有一些其它的小規則。
在類外定義的函數,可以將其默認參數與聲明組合,但不能將成員函數變成構造函數。
虛函數的默認參數由靜態類型決定。
除了調用運算符以外,其它的運算符都不能有默認參數。
對於在不同翻譯單元內定義的內聯函數,那么在每個翻譯單元末尾,默認參數集都需要相同。
友元函數聲明如果有默認參數,那么這個聲明必須是定義並且在這個翻譯單元中不會再有別的聲明