C 可變長引數 VS C++11 可變長模板

發表於2016-12-17

有些時候,我們定義一個函式,可能這個函式需要支援可變長引數,也就是說呼叫者可以傳入任意個數的引數。比如C函式printf().

我們可以這麼呼叫。

那麼這個函式是怎麼實現的呢?其實C語言支援可變長引數的。

我們舉個例子,

上面這個函式,接受變長引數,用來把所有輸入引數累加起來。可以這麼調:

那麼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)

typename…就表示一個模板引數包。可以這麼來例項化模板:

再來看一個更加具體的例子:

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可以通過遞迴來實現真正的可變長的。看下面的程式碼。

如果我們執行這段程式碼,會發現建構函式被呼叫了3次。第一次得到的型別是float,第二次是char,第三次是int。這就好像模板例項化的時候層層展開了。實際上也就是這麼一回事情。這裡使用了C++模板的特化來實現了遞迴,每遞迴一次就得到一個型別。看一下物件car裡面有什麼:

可以清晰的看到car裡面有三個head。基類裡面的head是float,第二個head是char,第三個head是int。

有了這個基礎之後,我們就可以實現我們的可變長模板類了,std::tuple就是個很好的例子。可以看看它的原始碼,這裡就不再介紹了。

可變長模板不光可以用於類的定義,也可以使用者函式模板。接下來,就用可變長引數來實現一個Sum函式,然後跟上面的C語言版本做對比。

可變長模板實現Sum函式

直接看程式碼:

在上面的程式碼裡面,可以很清楚的看到遞迴。

這條呼叫程式碼同樣得到結果10.這樣過程可以理解為,邊界條件的函式先執行完畢,然後4.0的執行完畢,再3.0,2.0,1.0以此被執行完畢。一個典型的遞迴。

ok,那麼跟C語言版本相比,又有哪些好處呢?

變長模板優點

之前提到的幾個C語言版本的主要缺點:

1. 引數個數,那麼對於模板來說,在模板推導的時候,就已經知道引數的個數了,也就是說在編譯的時候就確定了,這樣編譯器就存在可能去優化程式碼。

2. 引數型別,推導的時候也已經確定了,模板函式就可以知道引數型別了。

3. 既然編譯的時候就知道引數個數和引數型別了,那麼呼叫約定也就沒有限制了。

來實驗一下第二點吧

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++還是需要的,

用可變長模板就沒這個問題了,就算引數型別不一樣,只要對應的型別有對應的操作,就沒問題。當然像上面的例子,如果沒有過載+,那麼編譯的時候就報錯,這不就是我們需要的嗎?

附:

相關文章