代码模板在哪里_C++的可变参数模板
背景
一切都從函數傳參開始說起。我們知道,在C語言中有個神奇的函數:printf:
printf("%s : %dn","gemfield number",7030);這個函數可以傳遞可變參數,說到“可變”參數,主要是指兩點可變:1,參數數量可變;2,參數類型可變。比如上面演示的C庫中的printf,數量是可變的,類型也是可變的。
多么好玩的函數參數定義啊!參數的數量居然可變,類型居然也可變!那我們自己可以實現一個類似的函數嗎?可以,借助C語言提供的va_list、va_start、va_arg、va_end宏,可以輕松實現類似的可變參數。
va_arg:宏定義,用來獲取下一個參數 va_start:宏定義,開始使用可變參數列表 va_end:宏定義,結束使用可變參數列表 va_list:類型,存儲可變參數的信息我們用代碼試試吧,Gemfield定義了一個syszuxPrint函數,第一個參數指明參數的個數,第二個參數...是parameter pack:
#include <cstdarg> void syszuxPrint(int n, ...){va_list args;va_start(args, n);while(n--){std::cout<<va_arg(args, int)<<", ";}va_end(args);std::cout<<std::endl; } int main(int argc, char** argv) {syszuxPrint(3, 719,7030,27030); }程序完美的打印出了:“719, 7030, 27030,”。等等,這里只是實現了參數的可變,參數類型如何可變呢?其實也可以,主要到va_arg宏的第二個參數int了嗎?這種硬編碼限制了目前我們只能傳遞int類型。我們稍微簡陋的改造下上面的代碼:
#include <cstdarg> void syszuxPrint(int n, ...){va_list args;va_start(args, n);while(n--){if(n == 0){std::cout<<va_arg(args, const char*)<<", ";continue;}std::cout<<va_arg(args, int)<<", ";}va_end(args);std::cout<<std::endl; } int main(int argc, char** argv) {syszuxPrint(3, 719,7030,"civilnet"); }程序就可以打印出了字符串了:“719, 7030, civilnet,”,好了,目前我們已經初步的、簡陋的實現了參數的數量可變和參數類型的可變。經歷了上面這一過程,我們就自然而然的知道了一個真相,那就是printf這樣的函數,為什么第一個參數總是“格式化字符串”,就像文章開頭printf("%s : %dn","gemfield number",7030);中的"%s : %dn"?因為:格式化字符串告訴了va_*宏這兩點關鍵信息:可變參數的個數(百分號的個數)、可變參數的類型(%s、%d等)。而C++的可變參數模板克服了這些限制!
可變參數模板的基礎原理
C++的可變參數模板是怎么做到不需要告訴參數個數,也不需要告訴參數類型的呢?這仰仗于C++以下的功能:
1,基礎語法和例子說明
可變參數模板的關鍵字沿用了C語言的ellipsis(...),并且在3種地方進行了使用:
void syszuxPrint(){std::cout<<std::endl;}template<typename T, typename... Ts> void syszuxPrint(T arg1, Ts... arg_left){std::cout<<arg1<<", ";syszuxPrint(arg_left...); }int main(int argc, char** argv) {syszuxPrint(719,7030,"civilnet"); }哇,看起來比C的實現要簡單漂亮多了(還沒提到其它好處!)Gemfield先做個簡單解釋:
在上述代碼中,main函數里調用了syszuxPrint(719,7030,"civilnet");會導致syszuxPrint函數模板首先展開為:
void syszuxPrint(int, int, const char*)在打印第1個參數719后,syszuxPrint遞歸調用了自己,傳遞的參數為arg_left...,該參數會展開為【7030,"civilnet"】,syszuxPrint第2次進行了展開:
void syszuxPrint(int, const char*)在打印第1個參數7030后,syszuxPrint遞歸調用了自己,傳遞的參數為arg_left...,該參數會展開為【"civilnet"】,syszuxPrint第3次進行了展開:
void syszuxPrint(const char*)在打印第1個參數"civilnet"后,syszuxPrint遞歸調用了自己,傳遞的參數為arg_left...,該參數會展開為【】,syszuxPrint準備進行第4次展開:void syszuxPrint(),但是,我們已經定義了這個函數:
void syszuxPrint(){std::cout<<std::endl;}上面這個函數是函數模板syszuxPrint的“非模板重載”版本,于是展開停止,直接調用這個“非模板重載”版本,遞歸停止。
2,換個花樣重載
上面的例子里有個syszuxPrint的“非模板重載”版本,目的就是為了遞歸能夠最終退出,基于這個原理,我們也可以按照如下方式重新實現:
template<typename T> void syszuxPrint(T arg){std::cout<<arg<<", "<<std::endl;}template<typename T, typename... Ts> void syszuxPrint(T arg1, Ts... arg_left){std::cout<<arg1<<", ";syszuxPrint(arg_left...); }int main(int argc, char** argv) {syszuxPrint(719,7030,"civilnet"); }這里不再有syszuxPrint的“非模板重載”版本了,而是兩個函數模板,區別是模板參數的區別:當兩個參數模板都適用某種情況時,優先使用沒有“template parameter pack”的版本。
3,sizeof...操作符和一些“勇敢但錯誤”的想法
上面定義的syszuxPrint可變參數模板比起C語言的VA_*來說,不知道是好到哪里去了,但是還有一點讓人不舒服:它總是需要定義2次,目的只是為了讓遞歸退出。有沒有更優雅的辦法呢?
C++11引入了sizeof...操作符,可以得到可變參數的個數(注意sizeof...的參數只能是parameter pack,不能是其它類型的參數啊),如下所示:
std::cout<<"DEBUG: "<<sizeof...(Ts)<<" | "<<sizeof...(arg_left)<<std::endl;這樣可以打印出parameter的個數。你一定這樣想了,如果通過sizeof...判斷出arg_left這個parameter pack的個數為零了,那就退出遞歸調用不好嗎?就像下面這樣:
template<typename T, typename... Ts> void syszuxPrint(T arg1, Ts... arg_left){std::cout<<arg1<<", ";if(sizeof...(arg_left) > 0){syszuxPrint(arg_left...);} }int main(int argc, char** argv) {syszuxPrint(719,7030,"civilnet"); }你看,syszuxPrint只定義了1次,我們在函數體里使用了sizeof...,如果parameter pack為零了,我們就停止遞歸調用。但不幸的是,編譯程序報錯:
civilnet.cpp: In instantiation of ‘void syszuxPrint(T, Ts ...) [with T = const char*; Ts = {}]’: civilnet.cpp:211:20: recursively required from ‘void syszuxPrint(T, Ts ...) [with T = int; Ts = {const char*}]’ civilnet.cpp:211:20: required from ‘void syszuxPrint(T, Ts ...) [with T = int; Ts = {int, const char*}]’ civilnet.cpp:217:36: required from here civilnet.cpp:211:20: error: no matching function for call to ‘syszuxPrint()’211 | syszuxPrint(arg_left...);| ~~~~~~~~~~~^~~~~~~~~~~~~ civilnet.cpp:208:6: note: candidate: ‘template<class T, class ... Ts> void syszuxPrint(T, Ts ...)’208 | void syszuxPrint(T arg1, Ts... arg_left){| ^~~~~~~~~~~ civilnet.cpp:208:6: note: template argument deduction/substitution failed: civilnet.cpp:211:20: note: candidate expects at least 1 argument, 0 provided211 | syszuxPrint(arg_left...);| ~~~~~~~~~~~^~~~~~~~~~~~~核心錯誤是這句【civilnet.cpp:211:20:error: no matching function for call to ‘syszuxPrint()’】,啥意思啊?為什么還在試圖調用空的syszuxPrint?為啥sizeof...那個if條件表達式沒有生效?
這是因為,可變參數模板syszuxPrint的所有分支都被instandiated了,并不會考慮上面那個if表達式。一個instantiated的代碼是否有用是在runtime時決定的,而所有的instantiation是在編譯時決定的。所以syszuxPrint()空參數版本照樣被instandiated,而當instandiated的時候并沒有發現對應的實現,于是編譯期報錯。
4,C++17的if constexpr表達式和夢想實現
C++17中引入了編譯期if表達式(if constexpr),可以用來完美的解決這個問題:
template<typename T, typename... Ts> void syszuxPrint(T arg1, Ts... arg_left){std::cout<<arg1<<", ";if constexpr(sizeof...(arg_left) > 0){syszuxPrint(arg_left...);} }int main(int argc, char** argv) {syszuxPrint(719,7030,"civilnet"); }上面的代碼中,我們使用了if constexpr,完美編譯成功:g++ -std=c++17 civilnet.cpp -o civilnet。注意-std=c++17,因為Gemfield所用的g++還沒有默認啟用C++17的feature。
Fold表達式(C++17的feature)
C++17中引入了Fold表達式,如下所示:
template<typename... T> auto syszuxSum(T... s){return (... + s); } int main(int argc, char** argv) {syszuxSum(719,7030,27030); }當binary operator和parameter pack結合起來后,可以自動循環執行計算。像上述的... + s展開后,就相當于( (719 + 7030) + 27030)。Fold表達式有如下四種形式:
這里的op幾乎是所有的binary operator都可以,不止是加減乘除,甚至是 指針操作,甚至是<<,我們現在再來簡化下上面的syszuxPrint可變參數模板:
template<typename... Ts> void syszuxPrint(Ts... arg_left){(std::cout<< ... << arg_left) << std::endl; }int main(int argc, char** argv) {syszuxPrint(719,7030,"civilnet"); }我去,已經這么簡化和優雅了嗎?!只不過還有個小遺憾,就是這種情況下,輸出的元素之間沒有前文中那種分隔符了,這倒是也可以解決,只不過解決后代碼就沒現在這樣看起來簡潔和震撼了,所以Gemfield不在這里演示了。
Variadic Expressions (可變參數表達式)
你還可以對可變參數進行批量計算(這個可不是C++17的feature)。請看下面的例子:
template<typename... T> auto foldSum(const T&... s){syszuxPrint(s + s...); } int main(int argc, char** argv) {foldSum(719,7030, std::string("CivilNet")); }注意看其中的syszuxPrint(s + s...)用法,真是讓人瞠目結舌啊。這個表達式就相當于syszuxPrint( (719 + 719), (7030+7030), (string("CivilNet)+string("CivilNet)) )。同理:syszuxPrint(1 + s...)相當于將參數展開后的每個參數加1。注意這里的語法:
Gemfield還是喜歡syszuxPrint((1 + s)...) 和syszuxPrint((s + 1)...) 這樣的寫法,看起來更易讀一些。
Variadic Indices(可變參數索引)
索引操作也可以和可變參數語法結合起來:
template<typename C, typename... Idx> auto testVariadicIndices(const C& c, Idx... idx){syszuxPrint(c[idx]...); } int main(int argc, char** argv) {std::vector<std::string> vec{"gemfield","is","a","civilnet","maintainer"};testVariadicIndices(vec,0,3,4); }注意這個語法:syszuxPrint(c[idx]...),程序會打印出【gemfield, civilnet, maintainer,】。你也可以使用nontype模板參數來改造下上面的程序:
template< int... idx, typename C> auto testVariadicIndices(const C& c){syszuxPrint(c[idx]...); } int main(int argc, char** argv) {std::vector<std::string> vec{"gemfield","is","a","civilnet","maintainer"};testVariadicIndices<0,3,4>(vec); }Variadic Class Templates (可變參數類模板)
可變參數模板可以同樣作用到類模板上。一個重要的例子就是Tuple:
template<typename... Elements> class Tuple; ...... Tuple<int, std::string, char> t;t現在就可以保存integer、string、character類型的數據。
在著名的日志庫log4debug/gemfield.h中:
https://github.com/CivilNet/Gemfield/blob/master/src/cpp/log4debug/gemfield.h?github.com就使用了可變參數類模板。你可以去看看。
Variadic Base Blasses(可變參數基類)
這個名字就很直截了當了,在類的繼承體系中,基類也可以是可變參數,如下所示:
class Gemfield {public:void test1(){std::cout<<"This is base class Gemfield."<<std::endl;} };class CivilNet {public:void test2(){std::cout<<"This is base class CivilNet."<<std::endl;} };template<typename... Bases> class SYSZUX : public Bases...{};int main(int argc, char** argv) {SYSZUX<Gemfield, CivilNet> syszux;syszux.test1();syszux.test2(); }注意上述代碼中的public Bases... 語法,基類處出現了可變參數。程序輸出:
gemfield@ThinkPad-X1C:~$ ./civilnet This is base class Gemfield. This is base class CivilNet.總結
以上是生活随笔為你收集整理的代码模板在哪里_C++的可变参数模板的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 没有运行 spring_Spring事务
- 下一篇: linux系统sql语句报错_在linu
