C++11
聲明
C++11之前,
模板(類模板與函式模板)在聲明時必須有 固定數量的模板參數。C++11允許模板定義有任意類型任意數量的模板參數。
template<typename... Values> class tuple;
上述模板類tuple可以有任意個數的類型名(typename)作為它的模板形參(template parameter)。例如,上述模板類可以實例化具有3個類型實參(type argument)為:
tuple<int, std::vector<int>, std::map<<std::string>, std::vector<int>>> some_instance_name;
也可以有0個實參,如tuple<> some_instance_name;也是可以的。如果不希望可變參數模板有0個模板實參,可以如下聲明:
template<typename First, typename... Rest> class tuple;
可變參數模板也適用於函式模板,這不僅給可變參數函式(variadic functions,如printf)提供了類型安全的附加機制(add-on),還允許類似printf的函式處理不平凡對象。例如:
template<typename... Params> void printf(const std::string &str_format, Params... parameters);
使用
省略號出現形參名字左側,聲明了一個參數包(parameter pack)。使用這個參數包,可以綁定0個或多個模板實參給這個可變模板形參參數包。參數包也可以用於非類型的模板參數。
省略號出現包含參數包的表達式的右側,則把這個參數包解開為一組實參,使得在省略號前的整個表達式使用每個被解開的實參完成求值,所有表達式求值結果被逗號分開。注意這裡的逗號不是作為
逗號運算符,而是用作:
被逗號分隔開的一組函式調用實參列表;(該函式必須是可變參數函式,而不能是固定參數個數的函式)
被逗號分隔開的一組初始化器列表(initializer list);
被逗號分隔開的一組基類列表(base class list)與構造函式初始化列表(constructor's initialization list);
被逗號分隔開的一組函式的可拋出的異常規範(exception specification)的聲明列表。
具體例子見下文。實際上,能夠接受可變參數個數的參數包展開的場合,必須是能接受任意個數的逗號分隔開的表達式列表,這也就是上述四種場合。
可變參數模板經常遞歸使用。可變模板參數自身並不可直接用於函式或類的實現。例如,printf的C++11可變參數的替換版本實現:
void printf(const char *s){ while (*s) { if (*s == '%') { if (*(s + 1) == '%') { ++s; } else { throw std::runtime_error("invalid format string: missing arguments"); } } std::cout << *s++; }}template<typename T, typename... Args>void printf(const char *s, T value, Args... args){ while (*s) { if (*s == '%') { if (*(s + 1) == '%') { ++s; } else { std::cout << value; printf(s + 1, args...); // call even when *s == 0 to detect extra arguments return; } } std::cout << *s++; } throw std::logic_error("extra arguments provided to printf");}
這是一個遞歸實現的模板函式。注意這個可變參數模板實現的printf調用自身或者在args...為空時調用基本實現版本。
沒有簡單機制去在可變模板參數的每個單獨值上疊代。幾乎沒有什麼方式可以把參數包轉為單獨實參來使用。通常這靠函式重載,或者當函式可以每次撿出一個實參時用啞擴展標記(dumb expansion marker):
#include <iostream> template<typename type>type print(type param){ std::cout<<param<<' '; return param;}template<typename... Args> inline void pass(Args&&...) {}template<typename... Args> inline void expand(Args&&... args) { pass( print(args)... );}int main(){ expand(42, "answer", true);}
上例中的"pass"函式是必須的,因為參數包用逗號展開後只能作為被逗號分隔開的一組函式調用實參,而不是作為
逗號運算符,從而"pass"函式所能接受的調用實參個數必須是可變的,也即"pass"函式必須是可變參數函式。print(args)...;編譯不能通過。 此外,上述辦法要求print的返回類型不能是void;且所有對print的調用在一個非確定的順序,因為函式實參求值的順序是不確定的。如果要避免這種不確定的順序,可以用
大括弧封閉的初始化器列表(initializer list),這保證了嚴格的從左到右的求值順序。為避免void返回類型帶來的麻煩,使用逗號運算符使得每個擴展元素總是返回1。例如:
#include <iostream>template<typename T> void some_function(T value){ std::cout<<value<<' ';}template<typename... Args> inline void expand(Args&&... args) { int arr[]{(some_function(args),1 )...}; std::cout<<std::endl<<sizeof(arr)/sizeof(int); //也可以用sizeof...(Args)運算符}int main(){ expand(42, "answer", true);}
GCC尚不支持lambda表達式包含為展開的參數包,因此下述語句編譯不通過:
int arr[]{([&]{ std::cout << args << std::endl; }(), 1)...};
Visual C++ 2013支持上述風格的語句。當然,這裡的lambda函式不是必需的,通常的表達式即可:
int arr[]{(std::cout << args << std::endl, 1)...};
另一種方法使用重載函式的遞歸的終結版("termination versions")函式。這更為通用,但要求更多努力寫更多代碼。一個函式要求某種類型的實參與參數包。另一個函式沒有參數。如下例:
int func() {} // termination versiontemplate<typename Arg1, typename... Args>int func(const Arg1& arg1, const Args&... args){ process( arg1 ); func(args...); // note: arg1 does not appear here!}
如果args...包含至少一個實參,則將調用第二個版本的函式;如果參數包為空將調用第一個“終結”版的函式。可變參數模板可用於異常規範(exception specification)、基類列表(base class list)、構造函式初始化列表(constructor's initialization list)。例如:
template <typename... BaseClasses> class ClassName : public BaseClasses... {public: ClassName (BaseClasses&&... base_classes) : BaseClasses(base_classes)... {}};
上例中,實參列表被解包給TypeToConstruct的構造函式。std::forward<Args>(params)的句法是以適當的類型轉發實參。解包運算元將把轉發語法套用於每個參數。模板參數包中實參的個數可以如下確定:
template<typename ...Args> struct SomeStruct { static const int size = sizeof...(Args);};
例如SomeStruct<Type1, Type2>::size為2,SomeStruct<>::size為0。需要注意,sizeof...與sizeof是兩個不同的運算符。