Learning to be Giant.

Variadic Templates

|

在新标准当中,C++0x添加了一个叫做variadic templates的特性,目的在于实现可变参数的模板以及可变参数的函数。通过这种方法更加提升了C++语言的灵活性,特别是为类库的设计提供了很多的方便。在最近阅读的代码当中,多次出现了这个新的语言特性。经过一番研究,觉得有必要记录下来。

##基本用法和相关概念

template <class... Element> class tuple;
tuple<int, string> a;

class后面出现了三个点...,通过这三个点实现了对于Variadic template的定义。变化的参数被称为Parameter PackParameter Pack分为两种:1. template parameter pack 2. function parameter pack。在本里当中的Element便是第一种,即template parameter pack。

我们同样可以这么定义一个函数:

// Args is a template parameter pack; rest is a function parameter pack 
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);

在这个函数当中我们看到,Args是一个template parameter pack,而rest则是一个function parameter pack。每一个pack都可以表示零个或多个参数。

我们如何获知传进来了多少个参数呢?这里可以使用sizeof...sizeof...是新标准引进的一个新的操作符,可以获知parameter pack的长度。用法如:

template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest) {
	cout << sizeof...(rest) << endl;
}

foo(20, "string", nullptr); // 输出2

##编写Variadic Functions和Variadic Classes

编写Variadic的函数和类往往都需要利用到递归的思想。我们先看一个例子:

// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined 
template<typename T>
ostream &print(ostream &os, const T &t)
{
	return os << t; // no separator after the last element in the pack 
}
// this version of print will be called for all but the last element in the pack 
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) {
	os << t << ", "; // print the first argument
	return print(os, rest...); // recursive call; print the other arguments 
}

在本例当中,我们实现了一个接受不定个数参数并将他们输出的一个打印函数。本例同样摘自C++ Primer。我们可以这样调用这个函数:

print(std::cout, "hello", 2, nullptr, "world");

我们可以看出,事实上两个print函数是重载的关系,那么当我们调用的时候,编译器是如何决定调用哪个的呢?其实和一般情况下编译器决议调用哪一个函数的流程是一样的,在这里不详述了。在本例当中,在我们调用方法的语句当中跟了5个参数。除去第一个为ostream之外,剩下的4个便是我们希望打印的。我们看到这两个函数,只有第二个print可以接收4个参数,第一个只能接收1个,所以在这里调用的是第二个函数。

在第二个函数当中的return print(os, rest...);又一次递归调用了print。这一次,其参数事实上变成了print(os, 2, nullptr, "world"),依然只有第二个print能够满足。以此类推。直到又一次递归调用print的时候,其参数表变成了print(os,"world");。此时,第一个print变成了更好的匹配(虽然第二个print也可以匹配,因为parameter pack可以表示0个参数,但是第一个print比第二个要更加的specified,所以决议为第一个)。

通过上面这个例子,我们很容易理解其递归调用的思想。第一个print其实就是递归的终止条件。

我们再看一个定义Variadic Class的例子:

#include <string>
#include <iostream>

template <class ...Elements> class tuple;
template <class Head, class ...Tail>
class tuple<Head, Tail...> : private tuple<Tail...> {
    Head head;
public:
    tuple(Head head, Tail... tails) : tuple<Tail...>(tails...){
        std::cout << sizeof...(tails) << std::endl;
        this->head = head;
    }
    void print() {
        std::cout << head << " ";
        tuple<Tail...>::print();
    }
};

template<>
class tuple<> {
public:
    void print() {
        std::cout << std::endl;
    }
};

int main() {
    tuple<int, std::string, int*> t(1, std::string("hello"), nullptr);
    t.print();
}

通过对之前那个函数的例子的讲解,这个例子已经比较好懂。各位自己研究一下就好。该例的输出结果为:

0
1
2
1 hello 0x0 

Pack Expansion

Pack Expansion就是讲Pack扩展成为一系列构成这个Pack的元素。调用的方法为...

template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest)// expand Args 
{
	os << t << ", ";
	return print(os, rest...); // expand rest 
}

在本里当中,const Args&... rest)为对Args进行扩展,而print(os, rest...)为对rest进行扩展。我们可以将扩展理解为将这一个Pack解压缩,由一个Pack变成一串参数。例如:

print(std::cout, "hello", 2, nullptr, "world");

此时,rest...就会被扩展成为2,nullptr,"world"

这里我们需要了解一个概念,就是扩展的模式(Pattern)。事实上,我们不仅可以像const Args&... rest来定义,也可以const Args... rest或者甚至print(os, foo(rest)...)(foo是一个函数)。这里面蕴藏的意思是,pattern会被分别施加到“解压缩”出来的每一个元素身上。

还有一些高级主题

Primer当中,还介绍了Forwarding Parameter Packs这个技巧,其目的是在于获得可变参数表传进来的参数的真实类型,利用的是std::forward<>()。这里暂且不详述了。

参考文章

  1. http://www.cnblogs.com/liyiwen/archive/2013/04/13/3018608.html
  2. http://www.wuzesheng.com/?p=2103
  3. C++ Primer, 5th edition

Comments