可变参数模板
✍ dations ◷ 2025-09-07 06:36:15 #C++,计算机编程
可变参数模板是模板编程时,模板参数(template parameter)的个数可变的情形。
已经支持可变参数模板的编程语言有D语言与C++(自C++11标准)。
C++11之前,模板(类模板与函数模板)在声明时必须有 固定数量的模板参数。C++11允许模板定义有任意类型任意数量的模板参数。
例如,STL的类模板tuple可以有任意个数的类型名(typename)作为它的模板形参(template parameter):
template<typename... Values> class tuple;
如实例化为具有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);
用途
省略号(...)在可变参数模板中有两种用途:
具体例子见下文。实际上,能够接受可变参数个数的参数包展开的场合,必须是能接受任意个数的逗号分隔开的表达式列表,这也就是上述几种场合。
可变参数模板可递归使用。可变模板参数自身并不可直接用于函数或类的实现。例如,printf的C++11可变参数的替换版本实现:
void printf(const char *s) //已经没有额外的参数了,这里将要耗尽字符串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);}
另一种方法使用重载函数的递归的终结版("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)... {}};
这个例子中的解包算子将复制所有模板参数类型为ClassName
的基类型。构造函数取每个基类的引用,并初始化每个基类。
对于函数模板,可变模板参数可以转发(forward)。当与右值引用结合使用,这允许完美转发(perfect forwarding):
template<typename TypeToConstruct> struct SharedPtrAllocator { template<typename ...Args> std::shared_ptr<TypeToConstruct> construct_with_shared_ptr(Args&&... params) { return std::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...)); }};
上例中,实参列表被解包给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
是两个不同的运算符。
Lambda捕获例子:
template<class ...Args>void f(Args... args) { auto lm = { return g(args...); }; lm();}
编译器实现
GCC尚不支持lambda表达式包含为展开的参数包,因此下述语句编译不通过:
int arr{({ std::cout << args << std::endl; }(), 1)...};
Visual C++ 2013支持上述风格的语句。当然,这里的lambda函数不是必需的,通常的表达式即可:
int arr{(std::cout << args << std::endl, 1)...};
例子
下述代码实现了C++14引入的make_integer_sequence函数模板。它产生一个模板类,其模板参数为0,1,2,...,N。可用于生成或访问std::tuple
#include <iostream> // using aliases for cleaner syntax template<unsigned...> struct seq { using type = seq; };template<class S1, class S2> struct concat;template<unsigned... I1, unsigned... I2>struct concat<seq<I1...>, seq<I2...>> : seq<I1..., (sizeof...(I1) + I2)...> {};template<unsigned N>struct make_integer_sequence : concat<typename make_integer_sequence<N / 2>::type, typename make_integer_sequence<N - N / 2>::type>::type {};template<> struct make_integer_sequence<1> : seq<0> {};int printItem(unsigned k){ std::cout << k << ' '; return 0;}template<unsigned... I1>void printTemplate(seq<I1...> a){ int nn = { printItem(I1)... };}int main(){ make_integer_sequence<10> a; printTemplate(a);}
输出为
0 1 2 3 4 5 6 7 8 9
参见
更多文章关于可变参数结构而非模板:
相关
- β-变形菌纲详见细菌分类表β-变形菌网(学名:Betaproteobacteria)是变形菌门中的一纲,与γ-变形菌关系最近。医学导航:病菌细菌(分类)gr+f/gr+a(t)/gr-p(c/gr-o药物(J1p、w、n、m、疫苗)
- 日耳曼语强变化动词在日耳曼语言中,强变化动词是指借由源自印欧元音变换的母音变化来构造时态的动词。像英语的“sing-sang-sung”即其一例。英语的“Strong verb”一词译自德语的“Starkes Ver
- 奥克尼群岛奥克尼(英语:Orkney、苏格兰盖尔语:Arcaibh),是英国苏格兰东北部一群岛,南距苏格兰本土仅10英里左右,是苏格兰32行政区之一。该群岛由70个左右的岛屿组成,总面积990平方公里,其中20个
- 脑粘体虫脑粘体虫(英语:Myxobolus cerebralis)是一种寄生于鲑科(包括鲑鱼、鳟鱼及其同类)的粘孢子虫,可以导致养殖及野生的鲑鱼和鳟鱼发生旋转病。大约一个世纪前,在德国的虹鳟上首次发现了
- 北京大学药学院北京大学药学院,始建于1941年,原为北京大学医学院药学系;1952年医学院独立建院,随之改名为北京医学院药学系;1985年随学校更名为北京医科大学药学院,2000年4月北京医科大学与北京
- 澳大利亚人报《澳大利亚人报》(英语:The Australian)是澳大利亚销量最高的大报。日报流通量11万6千份,周末版流通量25万5千份。 该报2017年9月推出中文版。《澳大利亚人报》由新闻集团旗下的
- 埃及第十六王朝第
八第
十埃及第十六王朝是古埃及第二中间时期的一个王朝,以底比斯为中心,统治上埃及达70年之久。此王朝与第十五王朝、第十七王朝一起,共同组成第二中间时期。传统上认为第十
- 索尔·坎贝尔萨尔兹·杰里迈亚·"索尔"·坎贝尔(英语:Sulzeer Jeremiah "Sol" Campbell,1974年9月18日-)乃一名已退役的英格兰足球运动员,司职后卫。索尔·坎贝尔乃中美洲牙买加后裔,早年在托特
- 茨维瑟尔山 (基姆高山脉)坐标:47°45′21″N 12°48′43″E / 47.75583°N 12.81194°E / 47.75583; 12.81194茨维瑟尔山(德语:Zwiesel),是德国的山峰,位于该国南部,由巴伐利亚负责管辖,属于基姆高山脉的一
- 菊三七菊三七(学名:),又名黄花三七草,为菊科菊三七属的植物。分布于台湾、尼泊尔、泰国、日本以及中国大陆的四川、广西、安徽、云南、江西、贵州、陕西、湖北、福建、浙江、湖南等地,生