有些時候,我們定義一個函數,可能這個函數需要支持可變長參數,也就是說調用者可以傳入任意個數的參數。比如C函數printf().
我們可以這么調用。
printf("name: %s, number: %d", "Obama", 1);
那么這個函數是怎么實現的呢?其實C語言支持可變長參數的。
我們舉個例子,
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; }
上面這個函數,接受變長參數,用來把所有輸入參數累加起來。可以這么調:
double sum = Sum(4, 1.0, 2.0, 3.0, 4.0);
計算結果是10,很好。
那么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)
template<typename... A> class Car;
typename... 就表示一個模板參數包。可以這么來實例化模板:
Car<int, char> car;
再來看一個更加具體的例子:
template<typename T1, typename T2> class Car{}; template<typename... A>
class BMW : public Car<A...>{};
BMW<int, char> car;
在這個例子里面,BMW是一個可變參數的模板,它繼承於類Car. 那么BMW<int, char> car;在進行模板推導的時候,可以認為變成Car<int, char>了。這其中的功勞應該屬於A...
A... 稱之為包擴展(pack extension),包擴展是可以傳遞的。比如繼承的時候,或者直接在函數參數里面傳遞。然后當編譯器進行推導的時候,就會對這個包擴展進行展開,上面的例子,A...就展開成了int, char。
C++11定義了可以展開包的幾個地方:
- 1. 表達式
- 2. 初始化列表
- 3. 基類描述列表
- 4. 類成員初始化列表
- 5. 模板參數列表
- 6. 通用屬性列表
- 7. lamda函數的捕捉列表
其他地方是不能展開的。
針對上面的例子,如果我們改成BMW<int, char, int> car, 會如何呢?編譯的時候就直接報錯了,
Error 1 error C2977: 'Car' : too many template arguments d:\study\consoleapplication2\variablelengthparameters\variablelengthparameters.cpp27 1 VariableLengthParameters
這是因為當展開的時候,A...變成了int, char, int了,可能基類根本就沒有3個模板參數,所以推導就出錯了。
那如果這樣的話,可變長參數還是啥意義呢?這等於每次的參數個數還是固定的啊。當然不會這么傻,其實C++11可以通過遞歸來實現真正的可變長的。看下面的代碼。
////////////////////////////////////////////////////////////// #include <typeinfo> // 可變參數模板類:通過繼承+偏特化 展開參數包
template<typename... A> class BMW { public: BMW() { cout << "--------------------1" << endl; } }; // 模板特化 template<typename Head, typename... Tail> class BMW<Head, Tail...> : public BMW<Tail...> // { public: // BMW<int, char, float> 總會優先調用基類的構造函數,所以打印順序float->char->int BMW() { cout << "type: " << typeid(Head).name() << endl; } private: Head head; }; template<> class BMW<> { public: BMW() { cout << "--------------------2" << endl; } }; // 遞歸邊界條件 int mutableTemplateParamTest() { // 編譯時期即可確定參數個數和類型 BMW<int, char, float> mycar1; } //output: //--------------------2 //type: f //type: c //type: i
//////////////////////////////////////////////////////////////
// 可變參數模板類:通過模板偏特化和遞歸方式展開參數包
template<typename First, typename... Rest>
class Sum
{
public:
enum { value = Sum<First>::value + Sum<Rest...>::value };
};
template<typename Last>
class Sum<Last>
{
public:
enum{ value = sizeof(Last) };
};
//Sum<int, int> obj;
//cout << obj.value << endl;
//cout << Sum<int, int>::value << endl;
//output:
//8
//8
如果我們運行這段代碼,會發現構造函數被調用了3次。第一次得到的類型是float,第二次是char,第三次是int。這就好像模板實例化的時候層層展開了。實際上也就是這么一回事情。
這里使用了C++模板的特化來實現了遞歸,每遞歸一次就得到一個類型。看一下對象car里面有什么:
可以清晰的看到car里面有三個head。基類里面的head是float,第二個head是char,第三個head是int。
有了這個基礎之后,我們就可以實現我們的可變長模板類了,std::tuple就是個很好的例子。可以看看它的源代碼,這里就不再介紹了。
可變長模板不光可以用於類的定義,也可以用戶函數模板。接下來,就用可變長參數來實現一個Sum函數,然后跟上面的C語言版本做對比。
可變長模板實現Sum函數(通過遞歸方式展開參數包)
直接看代碼:
template<typename T1, typename... T2>
double Sum2(T1 p, T2... arg) { double ret = p + Sum2(arg...); return ret; } double Sum2() // 邊界條件
{ return 0; }
在上面的代碼里面,可以很清楚的看到遞歸。
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. 既然編譯的時候就知道參數個數和參數類型了,那么調用約定也就沒有限制了。
來實驗一下第二點吧
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++還是需要的,用可變長模板就沒這個問題了,就算參數類型不一樣,只要對應的類型有對應的操作,就沒問題。
當然像上面的例子,如果沒有重載+, 那么編譯的時候就報錯,這不就是我們需要的嗎?
附:
// 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; }
【轉自】http://blog.csdn.net/zj510/article/details/36633603