有些時候,我們定義一個函式,可能這個函式需要支援可變長引數,也就是說呼叫者可以傳入任意個數的引數。比如C函式printf().
我們可以這麼呼叫。
1 |
printf("name: %s, number: %d", "Obama", 1); |
那麼這個函式是怎麼實現的呢?其實C語言支援可變長引數的。
我們舉個例子,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
double Sum(int count, ...) { va_list ap; double sum = 0; va_start(ap, count); for (int i = 0; i < count; ++i) { double arg = va_arg(ap, double); sum += arg; } va_end(ap); return sum; } |
上面這個函式,接受變長引數,用來把所有輸入引數累加起來。可以這麼調:
1 |
double sum = Sum(4, 1.0, 2.0, 3.0, 4.0); |
那麼C語言的這個函式有什麼問題呢?
1. 函式本身並不知道傳進來幾個引數,比如我現在多傳一個引數,或者少傳一個引數,那麼函式本身是檢測不到這個問題的。這就可能會導致未定義的錯誤。
2. 函式本身也不知道傳進來的引數型別。以上面的例子,假如我把第二個引數1.0改成一個字串,又如何?答案就是會得到未定義的錯誤,也就是不知道會發生什麼。
3. 對於可變長引數,我們只能用__cdecl呼叫約定,因為只有呼叫者才知道傳進來幾個引數,那麼也只有呼叫者才能維持棧平衡。如果是__stdcall,那麼函式需要負責棧平衡,可是函式本身根本不知道有幾個引數,函式呼叫結束後,根本不知道需要將幾個引數pop out。(注:某些編譯器如VS,如果使用者寫了個__stdcall的可變長引數函式,VS會自動轉換成__cdecl的,當然這是編譯器乾的事情)
在C++語言裡面,在C++11之前,C++也只是相容了C的這種寫法,而C++本身並沒有更好的替代方案。其實對於C++這種強型別語言而言,C的這種可變長方案等於是開了個後門,函式居然不知道傳進來的引數是什麼型別。所以在C++11裡面專門提供了對可變長引數的更現代化的支援,那就是可變長模板。
模板引數包(template parameter pack)
1 |
template<typename... A> class Car; |
typename…就表示一個模板引數包。可以這麼來例項化模板:
1 |
Car<int, char> car; |
再來看一個更加具體的例子:
1 2 3 |
template<typename T1, typename T2> class Car{}; template<typename... A> class BMW : public Car<A...>{}; BMW<int, char> car; |
A…稱之為包擴充套件(pack extension),包擴充套件是可以傳遞的。比如繼承的時候,或者直接在函式引數裡面傳遞。然後當編譯器進行推導的時候,就會對這個包擴充套件進行展開,上面的例子,A…就展開成了int, char。C++11定義了可以展開包的幾個地方:
1. 表示式
2. 初始化列表
3. 基類描述列表
4. 類成員初始化列表
5. 模板引數列表
6. 通用屬性列表
7. lamda函式的捕捉列表
其他地方是不能展開的。
針對上面的例子,如果我們改成BMW car, 會如何呢?編譯的時候就直接報錯了,
Error 1 error C2977: ‘Car’ : too many template arguments d:studyconsoleapplication2variablelengthparametersvariablelengthparameters.cpp27 1 VariableLengthParameters
這是因為當展開的時候,A…變成了int, char, int了,可能基類根本就沒有3個模板引數,所以推導就出錯了。那如果這樣的話,可變長引數還是啥意義呢?這等於每次的引數個數還是固定的啊。當然不會這麼傻,其實C++11可以通過遞迴來實現真正的可變長的。看下面的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<typename... A> class BMW{}; template<typename Head, typename... Tail> class BMW<Head, Tail...> : public BMW<Tail...> { public: BMW() { printf("type: %s\n", typeid(Head).name()); } private: Head head; }; template<> class BMW<>{}; // 邊界條件 BMW<int, char, float> car; |
如果我們執行這段程式碼,會發現建構函式被呼叫了3次。第一次得到的型別是float,第二次是char,第三次是int。這就好像模板例項化的時候層層展開了。實際上也就是這麼一回事情。這裡使用了C++模板的特化來實現了遞迴,每遞迴一次就得到一個型別。看一下物件car裡面有什麼:
可以清晰的看到car裡面有三個head。基類裡面的head是float,第二個head是char,第三個head是int。
有了這個基礎之後,我們就可以實現我們的可變長模板類了,std::tuple就是個很好的例子。可以看看它的原始碼,這裡就不再介紹了。
可變長模板不光可以用於類的定義,也可以使用者函式模板。接下來,就用可變長引數來實現一個Sum函式,然後跟上面的C語言版本做對比。
可變長模板實現Sum函式
直接看程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
template<typename T1, typename... T2> double Sum2(T1 p, T2... arg) { double ret = p + Sum2(arg...); return ret; } double Sum2() // 邊界條件 { return 0; } |
在上面的程式碼裡面,可以很清楚的看到遞迴。
1 |
double ret2 = Sum2(1.0, 2.0, 3.0, 4.0); |
這條呼叫程式碼同樣得到結果10.這樣過程可以理解為,邊界條件的函式先執行完畢,然後4.0的執行完畢,再3.0,2.0,1.0以此被執行完畢。一個典型的遞迴。
ok,那麼跟C語言版本相比,又有哪些好處呢?
變長模板優點
之前提到的幾個C語言版本的主要缺點:
1. 引數個數,那麼對於模板來說,在模板推導的時候,就已經知道引數的個數了,也就是說在編譯的時候就確定了,這樣編譯器就存在可能去優化程式碼。
2. 引數型別,推導的時候也已經確定了,模板函式就可以知道引數型別了。
3. 既然編譯的時候就知道引數個數和引數型別了,那麼呼叫約定也就沒有限制了。
來實驗一下第二點吧
1 2 3 4 5 6 7 8 |
int _tmain(int argc, _TCHAR* argv[]) { double ret1 = Sum(4, 1.0, 2.0, 3.0, 4.0, "abcd"); double ret2 = Sum2(1.0, 2.0, 3.0, 4.0, "abcd"); return 0; } |
Sum是C語言版本,最後一個引數傳了個字串,但是Sum函式是無法檢測這個錯誤的。結果也就是未定義。
Sum2是個模板函式,最後一個引數也是字串,在編譯的時候就報錯了,
Error 1 error C2111: ‘+’ : pointer addition requires integral operandd:\study\consoleapplication2\variablelengthparameters\variablelengthparameters.cpp29
1 VariableLengthParameters
double無法和字串相加,這樣在編譯的時候就告訴我們這個錯誤了,我們就可以修復它,但是C語言的版本不會報錯,程式碼也就失控了,不知道會得到什麼結果。
怎麼樣,變長模板比C語言的變長引數好一些吧。
所以,我們還是儘可能使用C++11的變長模板吧。
最後一個問題,為什麼使用變長引數呢?有些人可能會問,是不是可以把所有的引數放到一個list裡面,然後函式遍歷整個list,再相加呢?good point,
如果所有的引數型別都一樣,確實可以這麼做,但是如果引數型別不一樣呢?那怎麼放到一個list裡面?像C++這種強型別語言可能做不到吧,確實弱型別語言比如php,python等,確實可以這麼做。根據我的理解,指令碼語言等弱型別語言不需要變長引數吧,或者不重要。但是C++還是需要的,
用可變長模板就沒這個問題了,就算引數型別不一樣,只要對應的型別有對應的操作,就沒問題。當然像上面的例子,如果沒有過載+,那麼編譯的時候就報錯,這不就是我們需要的嗎?
附:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
// VariableLengthParameters.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include "stdarg.h" #include <typeinfo> double Sum(int count, ...) { va_list ap; double sum = 0; va_start(ap, count); for (int i = 0; i < count; ++i) { double arg = va_arg(ap, double); sum += arg; } va_end(ap); return sum; } template<typename T1, typename... T2> double Sum2(T1 p, T2... arg) { double ret = p + Sum2(arg...); return ret; } double Sum2() { return 0; } template<typename... A> class BMW{}; template<typename Head, typename... Tail> class BMW<Head, Tail...> : public BMW<Tail...> { public: BMW() { printf("type: %s\n", typeid(Head).name()); } Head head; }; template<> class BMW<>{}; BMW<int, char, float> car; int _tmain(int argc, _TCHAR* argv[]) { double ret1 = Sum(4, 1.0, 2.0, 3.0, 4.0); double ret2 = Sum2(1.0, 2.0, 3.0, 4.0); return 0; } |