模板模板参数的一些用途是什么?

我已经看到一些c++使用模板模板参数(即将模板作为参数的模板)来进行基于策略的类设计的示例。这种技术还有什么其他用途?

205330 次浏览

我认为你需要使用模板模板语法来传递一个参数,它的类型是依赖于另一个模板的模板,就像这样:

template <template<class> class H, class S>
void f(const H<S> &value) {
}

在这里,H是一个模板,但我想让这个函数处理H的所有特化。

请注意:我已经编程c++很多年了,只需要这一次。我发现这是一个很少需要的功能(当然,当你需要它的时候,它很方便!)

我一直在努力想一些好的例子,老实说,大多数时候这是不必要的,但让我们想出一个例子。让我们假设std::vector 有一个typedef value_type

那么如何编写一个函数来为向量元素创建正确类型的变量呢?这是可行的。

template <template<class, class> class V, class T, class A>
void f(V<T, A> &v) {
// This can be "typename V<T, A>::value_type",
// but we are pretending we don't have it


T temp = v.back();
v.pop_back();
// Do some work on temp


std::cout << temp << std::endl;
}

请注意: std::vector有两个模板形参:type和allocator,所以我们必须同时接受它们。幸运的是,由于类型演绎,我们不需要显式地写出确切的类型。

你可以这样用:

f<std::vector, int>(v); // v is of type std::vector<int> using any allocator

或者更好的是,我们可以用:

f(v); // everything is deduced, f can deal with a vector of any type!

更新:即使是这个人为的例子,虽然是说明性的,但由于c++11引入了auto,它不再是一个令人惊讶的例子。现在同样的函数可以写成:

template <class Cont>
void f(Cont &v) {


auto temp = v.back();
v.pop_back();
// Do some work on temp


std::cout << temp << std::endl;
}

这就是我喜欢写这类代码的方式。

下面是Andrei Alexandrescu从现代c++设计——应用泛型编程和设计模式中获得的一个简单的例子:

他使用一个带有模板参数的类来实现策略模式:

// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
< p >他解释道: 通常,宿主类已经知道,或者可以很容易地推断出策略类的模板参数。在上面的例子中,WidgetManager总是管理Widget类型的对象,因此要求用户在CreationPolicy的实例化中再次指定Widget是多余的,而且有潜在的危险。在这种情况下,库代码可以使用template模板参数来指定策略

结果是客户端代码可以以一种更优雅的方式使用'WidgetManager':

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

而不是更麻烦,更容易出错的方式,一个定义缺乏模板模板参数将需要:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
下面是来自CUDA卷积神经网络库的另一个实际示例。 我有以下类模板:

template <class T> class Tensor

它实际上实现了n维矩阵操作。 还有一个子类模板:

template <class T> class TensorGPU : public Tensor<T>

实现相同的功能,但在GPU。 这两个模板都可以使用所有基本类型,如float, double, int等 我也有一个类模板(简化):

template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> >
{
TT<T> weights;
TT<T> inputs;
TT<int> connection_matrix;
}

这里使用模板模板语法的原因是我可以声明类的实现

class CLayerCuda: public CLayerT<TensorGPU, float>

它将在GPU上具有float类型的权重和输入,但connection_matrix将始终是int,无论是在CPU上(通过指定TT=Tensor)还是在GPU上(通过指定TT=TensorGPU)。

假设您正在使用CRTP为一组子模板提供一个“接口”;父类和子类在其他模板参数中都是参数:

template <typename DERIVED, typename VALUE> class interface {
void do_something(VALUE v) {
static_cast<DERIVED*>(this)->do_something(v);
}
};


template <typename VALUE> class derived : public interface<derived, VALUE> {
void do_something(VALUE v) { ... }
};


typedef interface<derived<int>, int> derived_t;

注意'int'的重复,这实际上是指定给两个模板的相同类型参数。你可以为DERIVED使用模板模板来避免重复:

template <template <typename> class DERIVED, typename VALUE> class interface {
void do_something(VALUE v) {
static_cast<DERIVED<VALUE>*>(this)->do_something(v);
}
};


template <typename VALUE> class derived : public interface<derived, VALUE> {
void do_something(VALUE v) { ... }
};


typedef interface<derived, int> derived_t;

请注意,您取消了直接向派生的模板提供其他模板参数;“接口”仍然接收它们。

这还允许您在依赖于类型参数的“接口”中构建类型defs,可以从派生的模板中访问。

上面的typedef不起作用,因为你不能对一个未指定的模板进行typedef。然而,这是可行的(c++ 11对模板类型defs有原生支持):

template <typename VALUE>
struct derived_interface_type {
typedef typename interface<derived, VALUE> type;
};


typedef typename derived_interface_type<int>::type derived_t;

不幸的是,派生模板的每个实例化都需要一个derived_interface_type,除非我还没有学到其他技巧。

实际上,模板模板参数的使用情况是相当明显的。一旦你了解到c++ stdlib有一个没有为标准容器类型定义流输出操作符的漏洞,你可以继续编写如下内容:

template<typename T>
static inline std::ostream& operator<<(std::ostream& out, std::list<T> const& v)
{
out << '[';
if (!v.empty()) {
for (typename std::list<T>::const_iterator i = v.begin(); ;) {
out << *i;
if (++i == v.end())
break;
out << ", ";
}
}
out << ']';
return out;
}

然后你会发现vector的代码是一样的,forward_list的代码也是一样的,实际上,即使对于很多map类型它也是一样的。这些模板类除了元接口/协议之外没有任何共同之处,使用template template形参可以捕获所有模板类中的共性。不过,在继续编写模板之前,有必要检查一个引用,以回顾序列容器接受2个模板参数——用于值类型和分配器。虽然allocator是默认的,但我们仍然应该在模板操作符<<中说明它的存在:

template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...

瞧,这将为所有遵循标准协议的当前和未来序列容器自动工作。要向混合类型中添加映射,可以查看一下reference,并注意到它们接受4个模板参数,因此我们需要另一个版本的操作符<<上面有4个参数的模板模板参数。我们还会看到std:pair尝试使用2参数操作符<<对于前面定义的序列类型,我们将为std::pair提供专门化。

顺便说一句,C+11允许可变参数模板(因此应该允许可变参数模板模板参数),可以有一个操作符<<去统治他们。例如:

#include <iostream>
#include <vector>
#include <deque>
#include <list>


template<typename T, template<class,class...> class C, class... Args>
std::ostream& operator <<(std::ostream& os, const C<T,Args...>& objs)
{
os << __PRETTY_FUNCTION__ << '\n';
for (auto const& obj : objs)
os << obj << ' ';
return os;
}


int main()
{
std::vector<float> vf { 1.1, 2.2, 3.3, 4.4 };
std::cout << vf << '\n';


std::list<char> lc { 'a', 'b', 'c', 'd' };
std::cout << lc << '\n';


std::deque<int> di { 1, 2, 3, 4 };
std::cout << di << '\n';


return 0;
}

输出

std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = float, C = vector, Args = <std::__1::allocator<float>>]
1.1 2.2 3.3 4.4
std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = char, C = list, Args = <std::__1::allocator<char>>]
a b c d
std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = int, C = deque, Args = <std::__1::allocator<int>>]
1 2 3 4

这是我遇到的情况:

template<class A>
class B
{
A& a;
};


template<class B>
class A
{
B b;
};


class AInstance : A<B<A<B<A<B<A<B<... (oh oh)>>>>>>>>
{


};

可解为:

template<class A>
class B
{
A& a;
};


template< template<class> class B>
class A
{
B<A> b;
};


class AInstance : A<B> //happy
{


};

或者(工作代码):

template<class A>
class B
{
public:
A* a;
int GetInt() { return a->dummy; }
};


template< template<class> class B>
class A
{
public:
A() : dummy(3) { b.a = this; }
B<A> b;
int dummy;
};


class AInstance : public A<B> //happy
{
public:
void Print() { std::cout << b.GetInt(); }
};


int main()
{
std::cout << "hello";
AInstance test;
test.Print();
}

在使用pfalcon提供的可变参数模板的解决方案中,由于可变参数专门化的贪婪性质,我发现很难实际专门化std::map的ostream操作符。下面是一个对我有用的小修改:

#include <iostream>
#include <vector>
#include <deque>
#include <list>
#include <map>


namespace containerdisplay
{
template<typename T, template<class,class...> class C, class... Args>
std::ostream& operator <<(std::ostream& os, const C<T,Args...>& objs)
{
std::cout << __PRETTY_FUNCTION__ << '\n';
for (auto const& obj : objs)
os << obj << ' ';
return os;
}
}


template< typename K, typename V>
std::ostream& operator << ( std::ostream& os,
const std::map< K, V > & objs )
{


std::cout << __PRETTY_FUNCTION__ << '\n';
for( auto& obj : objs )
{
os << obj.first << ": " << obj.second << std::endl;
}


return os;
}




int main()
{


{
using namespace containerdisplay;
std::vector<float> vf { 1.1, 2.2, 3.3, 4.4 };
std::cout << vf << '\n';


std::list<char> lc { 'a', 'b', 'c', 'd' };
std::cout << lc << '\n';


std::deque<int> di { 1, 2, 3, 4 };
std::cout << di << '\n';
}


std::map< std::string, std::string > m1
{
{ "foo", "bar" },
{ "baz", "boo" }
};


std::cout << m1 << std::endl;


return 0;
}

这是我用过的东西推广出来的。我发布它是因为它是一个非常简单的例子,它演示了一个实际的用例以及默认参数:

#include <vector>


template <class T> class Alloc final { /*...*/ };


template <template <class T> class allocator=Alloc> class MyClass final {
public:
std::vector<short,allocator<short>> field0;
std::vector<float,allocator<float>> field1;
};

它提高了代码的可读性,提供了额外的类型安全性,并节省了一些编译器的工作。

假设你想打印容器的每个元素,你可以使用下面的不带template参数的代码

template <typename T> void print_container(const T& c)
{
for (const auto& v : c)
{
std::cout << v << ' ';
}
std::cout << '\n';
}

或带模板模板参数

template< template<typename, typename> class ContainerType, typename ValueType, typename AllocType>
void print_container(const ContainerType<ValueType, AllocType>& c)
{
for (const auto& v : c)
{
std::cout << v << ' ';
}
std::cout << '\n';
}

假设你传入一个整数,比如print_container(3)。对于前一种情况,模板将由编译器实例化,编译器将抱怨在For循环中使用c,后者将根本不实例化模板,因为没有找到匹配的类型。

一般来说,如果你的模板类/函数被设计成将模板类作为模板形参处理,最好把它弄清楚。

我将它用于版本控制类型。

如果你有一个通过诸如MyType<version>这样的模板进行版本控制的类型,你可以编写一个函数来捕获版本号:

template<template<uint8_t> T, uint8_t Version>
Foo(const T<Version>& obj)
{
assert(Version > 2 && "Versions older than 2 are no longer handled");
...
switch (Version)
{
...
}
}
所以你可以根据传入类型的版本做不同的事情,而不是为每个类型重载。 你也可以有一个转换函数,它以通用的方式接受MyType<Version>并返回MyType<Version+1>,甚至递归它们有一个ToNewest()函数,它从任何旧版本中返回一个类型的最新版本(对于可能已经存储了一段时间但需要用今天的最新工具处理的日志非常有用)