上例中由于Stack类模板的声明中第二个参数是一个类型(typename Container
),所以我们通过Stack<int, std::deque<int>>
定义一个具体的栈类型时,第二个参数传递std::deque<int>
,而不能是std::deque
。上述定义中我们一共把int写了两遍,而这种重复是一种必然的重复。
为了避免上述重复,我们可以让Stack的第二个参数直接是一个模板,而不再是一个具体类型。
template<typename T,
template<typename> class Container = std::vector>
struct Stack
{
void push(const T& elem)
{
elems.push_back(elem);
}
T pop()
{
if(empty()) throw std::out_of_range("Stack<>::pop: empty!");
auto elem = elems.back();
elems.pop_back();
return elem;
}
bool empty() const
{
return elems.empty();
}
private:
Container<T> elems;
};
如上Stack类模板的第二个参数变为template<typename> class Container
,它的名字仍旧是Container,但是类型变为一个模板,这个模板具有一个类型参数。由于Container自身的模板形参名字没有被使用,所以我们可以省略。按照标准这里声明Container前的关键字只能是class,不能是typename。最后,模板的模板参数也可以有默认值,这里我们设置为std::vector
。
有了上面的定义,我们期望可以这样使用Stack:Stack<int, std::deque> intStack
,但编译器却给了我们一个教训。
std::deque类模板在stl库中的定义有两个类型参数,第一个参数是元素类型,第二个参数是分配器allocator的类型。虽然std::deque的第二个类型参数有默认值,但是当编译器使用std::deque替换Container时却会严格匹配参数,默认值被忽略了。
我们修改Stack的定义如下:
template<typename T,
template<typename Elem, typename Allocator = std::allocator<Elem>> class Container = std::vector>
struct Stack
{
void push(const T& elem)
{
elems.push_back(elem);
}
T pop()
{
if(empty()) throw std::out_of_range("Stack<>::pop: empty!");
auto elem = elems.back();
elems.pop_back();
return elem;
}
bool empty() const
{
return elems.empty();
}
private:
Container<T> elems;
};
现在Stack<int, std::deque> intStack
可以编译通过了!
可以看到模板的模板参数特性,可以让类模板之间通过模板参数互相组合。如果我们将类模板比作C++编译期的函数,那么可以接受模板作为参数的类模板,就相当于一个函数的入参仍旧可以是函数,这是后面我们会介绍到的高阶函数的概念。