我已经看到了一些使用模板模板参数(即以模板作为参数的模板)进行基于策略的类设计的C ++示例。这种技术有什么其他用途?
答案 0 :(得分:171)
我认为你需要使用模板模板语法来传递一个参数,其类型是依赖于另一个模板的模板,如下所示:
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;
}
这就是我更喜欢写这种代码的方式。
答案 1 :(得分:141)
实际上,模板模板参数的用例非常明显。一旦你了解到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是相同的,实际上,即使对于大量的地图类型它仍然是相同的。除了元接口/协议之外,这些模板类没有任何共同点,并且使用模板模板参数可以捕获所有模板类的共性。在继续编写模板之前,有必要检查引用以回忆序列容器接受2个模板参数 - 对于值类型和分配器。虽然分配器是默认的,但我们仍然应该在模板运算符&lt;&lt;:
中考虑它的存在template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...
Voila,它将自动为所有符合标准协议的现有和未来序列容器工作。要向混合添加地图,需要查看参考,注意它们接受4个模板参数,因此我们需要另一个版本的运算符&lt;&lt;上面有4-arg模板模板参数。我们还会看到std:pair尝试使用2-arg运算符&lt;&lt;对于我们之前定义的序列类型,我们只为std :: pair提供一个特化。
顺便说一句,C + 11允许可变参数模板(因此应该允许可变参数模板模板),可能有单个运算符&lt;&lt;统治他们。例如:
#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
答案 2 :(得分:60)
以下是Andrei Alexandrescu从'Modern C++ Design - Generic Programming and Design Patterns Applied'采取的一个简单示例:
他使用带有模板模板参数的类来实现策略模式:
// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
他解释说: 通常,主机类已经知道或可以很容易地推断出策略类的模板参数。在上面的示例中,WidgetManager始终管理Widget类型的对象,因此要求用户在CreationPolicy的实例化中再次指定Widget是多余的并且可能存在危险。在这种情况下,库代码可以使用模板模板参数来指定策略。
效果是客户端代码可以更优雅的方式使用'WidgetManager':
typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;
而不是缺少模板模板参数的定义所需的更加繁琐且容易出错的方式:
typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
答案 3 :(得分:18)
这是我CUDA Convolutional neural network library的另一个实际例子。 我有以下课程模板:
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>
它将具有float类型和GPU类型的权重和输入,但connection_matrix将始终为int,在CPU上(通过指定TT = Tensor)或在GPU上(通过指定TT = TensorGPU)。
答案 4 :(得分:10)
假设您正在使用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;
请注意,您正在取消直接向派生模板提供其他模板参数; “界面”仍然接收它们。
这也允许您在依赖于类型参数的“接口”中构建typedef,这些参数可以从派生模板访问。
上述typedef不起作用,因为您无法将typedef键入未指定的模板。但这很有效(并且C ++ 11对模板typedef有本机支持):
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,除非我还没有学到另一个技巧。
答案 5 :(得分:6)
这就是我遇到的:
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();
}
答案 6 :(得分:4)
在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;
}
答案 7 :(得分:2)
这是我刚刚使用过的东西的概括。我发布了它,因为它是一个非常的简单示例,它演示了一个实际的用例以及默认参数:
def process(self, data):
print "Got command {}, {}".format(data, len(data))
if data == "test":
print "DEBUG: got the test command"
from twisted.internet import reactor, protocol
class MudLoop(protocol.Protocol):
def connectionMade(self):
login(self)
def dataReceived(self, data):
process(self, data)
factory = protocol.ServerFactory()
factory.protocol = MudLoop
reactor.listenTCP(12000,factory)
reactor.run()
答案 8 :(得分:0)
它提高了代码的可读性,提供了额外的类型安全性并节省了一些编译工作。
假设您要打印容器的每个元素,可以使用以下代码而不使用模板模板参数
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
的使用,后者将根本不实例化模板,因为找不到匹配类型。
一般来说,如果您的模板类/函数被设计为将模板类作为模板参数处理,那么最好将其清楚。
答案 9 :(得分:0)
我将其用于版本控制类型。
如果您的类型通过模板python3 -m virtualenv venv
进行了版本控制,则可以编写一个函数来捕获版本号:
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>
函数,该函数返回任何类型的最新版本较旧的版本(对于可能已经存储了一段时间但需要使用当今最新工具进行处理的日志非常有用)。