在 C++11 之前, 不论是类模板或是函数模板,都只能按其被声明时所指定的样子,接受一组固定数目的模板参数 ; C++11 加入新的表示法,允许任意个数、任意类别的模板参数,不必在定义时将参数的个数固定。
template<typename... Values> class tuple;
模板类 tuple 的对象,能接受不限个数的 typename 作为它的模板形参:
class tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> someInstanceName;
实参的个数也可以是 0,所以 class tuple<> someInstanceName 这样的定义也是可以的。
若不希望产生实参个数为 0 的变长参数模板,则可以采用以下的定义:
template<typename First, typename... Rest> class tuple;
变长参数模板也能运用到模板函数上。 传统 C 中的 printf 函数,虽然也能达成不定个数的形参的调用,但其并非类别安全。 以下的样例中,C++11 除了能定义类别安全的变长参数函数外,还能让类似 printf 的函数能自然地处理非自带类别的对象。 除了在模板参数中能使用...表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数。
template<typename... Params> void printf(const std::string &strFormat, Params... parameters);
其中,Params 与 parameters 分别代表模板与函数的变长参数集合, 称之为参数包 (parameter pack)。参数包必须要和运算符"..."搭配使用,避免语法上的歧义。
变长参数模板中,变长参数包无法如同一般参数在类或函数中使用; 因此典型的手法是以递归的方法取出可用参数,参看以下的 C++11 printf 样例:
void printf(const char *s) { while (*s) { if (*s == '%' && *(++s) != '%') 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 == '%' && *(++s) != '%') { std::cout << value; printf(*s ? ++s : s, args...); // 即便当 *s == 0 也会产生调用,以检测更多的类型参数。 return; } std::cout << *s++; } throw std::logic_error("extra arguments provided to printf"); }
printf 会不断地递归调用自身:函数参数包 args... 在调用时, 会被模板类别匹配分离为 T value和 Args... args。 直到 args... 变为空参数,则会与简单的printf(const char *s) 形成匹配,退出递归。
另一个例子为计算模板参数的个数,这里使用相似的技巧展开模板参数包 Args...:
template<> struct count<> { static const int value = 0; }; template<typename T, typename... Args> struct count<T, Args...> { static const int value = 1 + count<Args...>::value; };
虽然没有一个简洁的机制能够对变长参数模板中的值进行迭代,但使用运算符"..."还能在代码各处对参数包施以更复杂的展开操作。举例来说,一个模板类的定义:
template <typename... BaseClasses> class ClassName : public BaseClasses... { public: ClassName (BaseClasses&&... baseClasses) : BaseClasses(baseClasses)... {} }
BaseClasses... 会被展开成类型 ClassName 的基底类; ClassName 的构造函数需要所有基类的左值引用,而每一个基类都是以传入的参数做初始化 (BaseClasses(baseClasses)...)。
在函数模板中,变长参数可以和左值引用搭配,达成形参的完美转送 (perfect forwarding):
template<typename TypeToConstruct> struct SharedPtrAllocator { template<typename... Args> std::shared_ptr<TypeToConstruct> ConstructWithSharedPtr(Args&&... params) { return tr1::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...)); } }
参数包 parms 可展开为 TypeToConstruct 构造函数的形参。 表达式std::forward<Args>(params) 可将形参的类别信息保留(利用右值引用),传入构造函数。 而运算符"..."则能将前述的表达式套用到每一个参数包中的参数。这种工厂函数(factory function)的手法, 使用 std::shared_ptr 管理配置对象的内存,避免了不当使用所产生的内存泄漏(memory leaks)。
此外,变长参数的数量可以藉以下的语法得知:
template<typename ...Args> struct SomeStruct { static const int size = sizeof...(Args); }
SomeStruct<Type1, Type2>::size 是 2,而 SomeStruct<>::size 会是 0。 (sizeof...(Args) 的结果是编译期常数。)