-
可變參數模板
鎖定
- 中文名
- 可變參數模板
- 外文名
- Variable parameter template
- 支持語言
- D語言,C++
- 領 域
- 計算機編程
可變參數模板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 version template<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); };