我已经看到一些c++使用模板模板参数(即将模板作为参数的模板)来进行基于策略的类设计的示例。这种技术还有什么其他用途?
当前回答
它提高了代码的可读性,提供了额外的类型安全性,并节省了一些编译器的工作。
假设你想打印容器的每个元素,你可以使用下面的不带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,后者将根本不实例化模板,因为没有找到匹配的类型。
一般来说,如果你的模板类/函数被设计成将模板类作为模板形参处理,最好把它弄清楚。
其他回答
我认为你需要使用模板模板语法来传递一个参数,它的类型是依赖于另一个模板的模板,就像这样:
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;
}
这就是我喜欢写这类代码的方式。
实际上,模板模板参数的使用情况是相当明显的。一旦你了解到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;
}
Then you'd figure out that code for vector is just the same, for forward_list is the same, actually, even for multitude of map types it's still just the same. Those template classes don't have anything in common except for meta-interface/protocol, and using template template parameter allows to capture the commonality in all of them. Before proceeding to write a template though, it's worth to check a reference to recall that sequence containers accept 2 template arguments - for value type and allocator. While allocator is defaulted, we still should account for its existence in our template operator<<:
template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...
瞧,这将为所有遵循标准协议的当前和未来序列容器自动工作。要将map添加到mix中,可以查看一下reference,并注意到它们接受4个模板参数,因此我们需要上述操作符<<的另一个版本,带有4个参数的template template param。我们还会看到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
下面是一个简单的例子,摘自Andrei Alexandrescu的《现代c++设计-泛型编程和设计模式的应用》:
他使用一个带有模板参数的类来实现策略模式:
// 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;
它提高了代码的可读性,提供了额外的类型安全性,并节省了一些编译器的工作。
假设你想打印容器的每个元素,你可以使用下面的不带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,后者将根本不实例化模板,因为没有找到匹配的类型。
一般来说,如果你的模板类/函数被设计成将模板类作为模板形参处理,最好把它弄清楚。
这是我的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)。
推荐文章
- 未定义对静态constexpr char的引用[]
- 在c++中,restrict关键字是什么意思?
- c++中类似于java的instanceof
- include_directories和target_include_directories在CMake中的区别是什么?
- std::make_pair与std::pair的构造函数的目的是什么?
- 如何追加一个字符到std::字符串?
- 为什么要在c++中使用嵌套类?
- 如何处理11000行c++源文件?
- 使用g++编译多个.cpp和.h文件
- 如何在c++中追加文本到文本文件?
- 在c++中使用"super
- Mmap () vs.读取块
- 什么是不归路?
- 将类代码分离为头文件和cpp文件
- 不能在双精度上使用模量?