可变参数模板

可变参数模板模板编程时,模板参数(template parameter)的个数可变的情形。

已经支持可变参数模板的编程语言D语言C++(自C++11标准)。

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);

用途

省略号(...)在可变参数模板中有两种用途:

  • 省略号出现在形参名字左侧,声明了一个参数包(parameter pack)[1]。使用这个参数包,可以绑定0个或多个模板实参给这个模板参数包。参数包也可以用于非类型的模板参数。
  • 省略号出现在包含参数包的表达式的右侧,称作参数包展开(parameter pack expansion),是把这个参数包解开为一组实参,使得在省略号前的整个表达式使用每个被解开的实参完成求值,所有表达式求值结果被逗号分开。这种表达式必须是可接受任意个数的以逗号分开的子表达式。注意这里的逗号不是作为逗号运算符,而是用作:
    • 被逗号分隔开的一组函数调用实参列表;(该函数必须是可变参数函数,而不能是固定参数个数的函数)
    • 被逗号分隔开的一组初始化器列表(initializer list);
    • 被逗号分隔开的一组基类列表(base class list)与构造函数初始化列表(constructor's initialization list);
    • 被逗号分隔开的一组函数的可抛出的异常规范(exception specification)的声明列表。
    • Lambda表达式的被逗号分隔开的用一对方括号包围的捕获。
    • 类模板或者函数模板定义中,
      • 类模板实参如果也是类模板并需要模板参数包作为实参,如template<unsigned... I1, unsigned... I2>struct concat<seq<I1...>, seq<I2...>>
      • 类模板的基类如果也是类模板,则其模板实参列表是逗号分隔的一个表达式,如:template<unsigned... I1, unsigned... I2>struct concat: seq<I1..., (sizeof...(I1) + I2)...>

具体例子见下文。实际上,能够接受可变参数个数的参数包展开的场合,必须是能接受任意个数的逗号分隔开的表达式列表,这也就是上述几种场合。

如果在一个参数包展开中同时出现了两个参数包的名字,则二者在一起同时展开,且应该具有相同长度。这可能出现在类模板带有一个参数包,它的嵌套的类模板或成员函数模板带有另一个参数包。

使用方法

可变参数模板可递归使用。可变模板参数自身并不可直接用于函数或类的实现。例如,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) {
    const int size = sizeof...(args) + 2;
    int arr[size]{1(some_function(args),1 )...2};
    std::cout<<std::endl<<sizeof(arr)/sizeof(int); //也可以用sizeof...(Args)运算符
}

int main()
{
      expand(42, "answer", true);
}

另一种方法使用重载函数的递归的终结版("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)... {}
};

这个例子中的解包算子将复制所有模板参数类型为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 = [&, args...] { return g(args...); };
    lm();
}

编译器实现

GCC尚不支持lambda表达式包含为展开的参数包,[2]因此下述语句编译不通过:

   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

参见

更多文章关于可变参数结构而非模板:

参考文献

  1. ^ cppreference: Parameter pack(since C++11). [2018-12-01]. (原始内容存档于2020-11-11). 
  2. ^ Bug 41933 - [c++0x] lambdas and variadic templates don't work together. GCC bugzilla database. Free Software Foundation. [8 December 2013]. (原始内容存档于2019-12-06). 

外部链接