可变模板参数是c++模板编程中一项极为强大的工具。我们一般将可变模板参数中的参数项称为模板参数包。`
//this is a basic declaration
of Variadic template parameters
template
void foo(Head vhead,tail ...vtail)
{
//some actions with vhead...
}
上图为一个较为经典的可变参模板函数声明,一个简单的调用如下图所示
#include"foo.hpp"
#include
int main()
{
std::string str;
int number;
char ch;
foo(str,number,ch);
return 0;
}
在此,我们以模板参数
str number ch
来实例化foo函数。在一般的语言结构里,我们会直觉地以为可以采用某种随机访问法访问可变模板参数包中的元素:
//implementation of foo//
//let us pretend that the vtail is
//what is above.
{
using myInt=tail[0];//Error:impossible
using myChar=tail[1];//Error:also impossible
int num=vtail[0];
int ch=vtail[1];//all impossible
}
这是很反直觉的。因为c++并不直接给我们提供手段对可变模板参数包进行随机访问,所以像如上的操作在语法层面便是不被允许的。
而我们又必须要对可变模板参数包进行某种访问,否则可变模板参数这个语法的存在不就毫无意义了吗?
这时候,我们必须引入递归。
考虑以下的实现,我们将foo单纯地设计为了打印出模板参数包中的每一个元素
//implementation of foo//
tempalte<typename Head,typename ...Tail>
void foo(Head vhead,Tail ...vtail)
{
std::cout<<vhead;
//success when Head
//has support for std::cout<<.
foo(vtail);
}
void foo()
{
//ends recursion
}
上述的程序做了什么?
还是如上的调用,我们首先打印vhead(str),然后将*vatil作为参数,递归地调用foo。
而我们又知道,模板的实例化都是在编译期完成的,所以如果我们站在上帝的视角,可以在编译器内看到如下展开:
//foo(str,num.ch);
//foo(num,ch);
//foo(ch);
//foo();//end recusion;
看明白了吗?我们每一次都递归地以可变模板参数包vtail调用foo,从而使这个参数包内的第一个元素成为栈顶foo的vhead,其类型成为栈顶foo的Head。
这一句话可能有些绕,但只要看懂了这一段,便能够理解递归与可变参数模板的关系。
而代码中饶有意思的一点在于,我们实现了一个普通的foo函数,以达到终止递归的效果,因为我们template-version 的 foo函数,并不接受0个参数。
这样的写法多多少少有些不雅观,而我们借用c++17提供的constexpr if 与sizeof…运算符可以轻易地优化上述代码:
//with c++17
template<typename Head,typename ...Tail>
void foo(Head vhead,Tail ...vtail)
{
std::cout<<vhead;//if Head supports std::cout<<
if constexpr(sizeof...(vtail)>0)
{
foo(vtail);
}
}
其中 sizeof…运算符会返回可变模板参数包中元素的个数,而编译期if则可以在c++17相关资料中进行了解。
说到这里,想必各位已对递归与可变模板参数的关系有了一个大概的了解。而我们则进入今天的重头戏:std::tuple。
想必了解过c++17特性的朋友们对于std::tuple并不陌生,想必于std::pair,std::tuple提供一种更为泛化的结构化返回值的手段。
//common usage of std::tuple
std::tuple<int,char,std::string>
t(1,'a',std::string("Hello,template!"));
auto v1=std::get<0>(t);//v1=1;int
auto v2=std::get<1>(t);//v2='a';char
auto v3=std::get<2>(t);//v3="Hello,template!";std::string
以上使用tuple的方式很常见1。
在我们仍是 C++ 新手时,everything was fine。我们很少会去考虑,tuple这个STL容器的神奇之处。
首先,std::tuple必然是一个可变参数模板类,因为他的元素可以由用户任意指定。
问题是不是就来了?在阅读上述知识以后,我们知道c++并不提供对于可变模板参数包中元素的随机访问。那对于std::tuple的实现机制,可以琢磨的东西便多了。
在此我只抽出两个问题:
正如侯捷老师所说:
源码面前了无秘密
//a very simple tuple:
template<typename Head,typename ...Tail>
class tuple
{
Head head;
tuple<Tail...> tail(vtail);
public:
tuple(Head vhead,Tail ...vtail);
Head getHead(){return head;};
auto getTail(){return tail;};
//...
};
template<>
class tuple<>
{
//...
};
一目了然。
同foo一样,tuple取出模板参数中的第一个参数Head,并将剩余的参数以参数包的形式递归地在内部构建tuple,并提供相应的接口返回头与尾。
有了如上声明,std::get的秘密就可以很好地被展现出来了:
template<std::size_t N>
struct TupleGet{
template<typename Head,typename...Tail>
static auto apply(tuple<Head,Tail...>const & t)
{
return TupleGet<N-1>::apply(t.getTail());
}
}
template<>//end recursion
struct TupleGet<0>
{
template<typename Head,typename...Tail>
static auto apply(tuple<Head,Tail...>const & t)
{
return t.getHead());
}
}
template<std::size_t N,typename ...types>
auto get(tuple<types> t)
{
return TupleGet<N>::apply(t);
}
下面简要地对代码进行说明:
//common usage of std::tuple
std::tuple<int,char,std::string>
t(1,'a',std::string("Hello,template!"));
auto v1=std::get<0>(t);//v1=1;int
auto v2=std::get<1>(t);//v2='a';char
auto v3=std::get<2>(t);//v3="Hello,template!";std::string
在如上代码段中,std::get<0>2,将会直接调用我们偏特化的版本,直接返回初始tuple的head
return t.getHead();
std::get<2>,将会实例化如下代码:
return TupleGet<2>::apply(t);
//t==tuple
//--------------------
//在apply,TupleGet<2>里:
return TupleGet<1>::apply(t.getTail());
//t==tuple.getTail();
//--------------------
//在apply,TupleGet<1>里:
return TupleGet<0>::apply(t.getTail());
//t==tuple.getTail().getTail();
//此时,调用偏特化版本的TupleGet<0>
{
return t.getHead();
}
示例代码的tuple一共只有三个元素,两次递归调用后即得到第三个元素的值,即为std::get<2>;
参考资料:《c++ templates》 第二版
赵善的善意
(尽管很多人认为std::tuple的相关接口十分丑陋,too pythonic maybe…,但不可否认std::tuple仍不失其强大)。 ↩︎
此处以我们的自定义get进行假设 ↩︎