模板模板参数有哪些用途?

316

我看过一些使用C++的模板模板参数(即以模板为参数的模板)来进行策略设计的例子。这种技术还有哪些用途?


6
我来自另一个方向(FP,Haskell等),并着陆在这里:https://dev59.com/W3E85IYBdhLWcg3w43sW - Erik Kaplun
10个回答

270

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

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 有两个模板参数,类型和分配器,因此我们必须接受它们两个。幸运的是,由于类型推断,我们不需要显式地写出精确的类型。

您可以像这样使用:

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
如果f是由库的用户定义的函数,那么用户需要将std::allocator<T>作为参数传递是很丑陋的。我本来期望没有std::allocator参数的版本可以使用std::vector的默认参数。关于C++0x方面是否有更新呢? - amit kumar
1
好的,您不必提供分配器。重要的是模板模板参数定义了正确数量的参数。但函数不应关心它们的“类型”或含义,在C++98中以下内容可以正常工作:template<template<class, class> class C, class T, class U> void f(C<T, U> &v) - pfalcon
我想知道为什么实例化是 f<vector,int> 而不是 f<vector<int>> - bobobobo
2
@bobobobo 这两者意义不同。f<vector,int> 表示 f<ATemplate,AType>,而 f<vector<int>> 表示 f<AType> - user362515
H” 是模板类型。 H 不是类型,vector 也不是类型。 它们是模板,而不是类型。 具体而言,vector 是一个类模板(而不是函数模板),而 vector<int> 则是一种模板类(即特定类型的类)。 - Aaron McDaid
显示剩余5条评论

197

实际上,模板模板参数的用法非常明显。一旦你了解到C++标准库存在一个巨大的漏洞,即没有为标准容器类型定义流输出运算符,你就可以编写类似以下内容的代码:

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;
}

那么你会发现,对于向量的代码来说只是一样的,对于前向列表也是一样的,实际上,即使是众多的映射类型也都是一样的。这些模板类除了元接口/协议之外没有任何共同点,使用模板模板参数可以在所有这些模板类中捕获共性。 在编写模板之前,最好检查一下参考文献以回忆一下序列容器接受2个模板参数-用于值类型和分配器。虽然分配器已经有默认值,但我们仍然应该考虑到它在我们的模板operator<<中的存在:

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

Voila,这将自动适用于所有遵循标准协议的现有和未来的序列容器。要将映射添加到混合中,需要查看参考以注意它们接受4个模板参数,因此我们需要上面的另一个版本,具有4个参数的模板模板参数的operator<<。我们还会看到,std:pair尝试使用我们之前定义的序列类型的2-arg operator<<进行呈现,因此我们将为std::pair提供专门化。
顺便说一下,使用允许可变模板(因此应该允许可变模板模板参数)的C + 11,可以拥有统治它们所有的单个operator<<。例如:
#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 

20
这是模板模板参数的一个很好的例子,因为它展示了每个人都不得不处理的情况。 - Ravenwater
4
这是我在C++模板中得到的最启发性答案。 @WhozCraig,你是如何获取模板展开的详细信息的? - Arun
4
gcc支持一个名为__PRETTY_FUNCTION__的宏,其中包括以纯文本形式报告模板参数描述的功能。clang也支持此功能。有时这是一个非常方便的功能(正如您所看到的)。 - WhozCraig
26
这里的模板模板参数并没有增加任何价值。你可以像使用类模板的任何实例一样,使用普通的模板参数。 - David Stone
13
我同意David Stone的观点。在这里,使用“template template parameter”是没有意义的。使用简单的模板(template <typename Container>)会更加简单而且同样有效。我知道这篇文章相当古老了,所以我只是为那些偶然找到这篇回答并寻找有关“template模板”的信息的人添加了我的2分意见。 - Jim Vargo
显示剩余9条评论

78

这里是一个简单的例子,摘自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;

2
这个问题明确要求提供除策略模式之外的示例。 - user2913094
我从这本书中正好看到了这个问题。值得注意的是,模板模板参数也出现在Typelist章节和使用Typelists生成Class章节中。 - Victor

22

以下是我 CUDA卷积神经网络库 的另一个实际示例。

我有以下类模板:

template <class T> class Tensor

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

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

该模板实现了与CPU相同的功能,但是它是在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上进行计算。但是,连接矩阵始终为int类型,可以在CPU上指定TT = Tensor或者在GPU上指定TT = TensorGPU来实现。


你能否使用类似于“template <class T, template <T> TT> CLayerT”和“class CLayerCuda: public CLayerT<TensorGPU<float>>”的方式强制推导出T?如果您不需要TT<otherT>。 - NicoBerrogorry
不用在意:template<template<class T> class U> class B1 { }; 来自 https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.cbclx01/template_template_arguments.htm 通过快速谷歌搜索获得。 - NicoBerrogorry

14

这就是我遇到的问题:

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();
}

13

假设您正在使用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"的重复使用,实际上是指定给两个模板的同一类型参数。您可以使用模板模板来避免此重复:

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不起作用,因为您无法使用未指定的模板进行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,除非有我还没学过的其他技巧。


我需要这个代码的确切解决方案(谢谢!)。虽然它可以工作,但我不明白模板类derived如何在没有其模板参数的情况下使用,即行typedef typename interface<derived, VALUE> type; - Carlton
@Carlton,它基本上能够工作是因为被填充的相应模板参数被定义为“template <typename>”。从某种意义上讲,您可以将模板参数视为具有“元类型”;模板参数的正常元类型是“typename”,这意味着它需要由常规类型填充;“template”元类型表示它需要用指向模板的引用来填充。 "derived" 定义了一个接受一个“typename”元类型参数的模板,因此它符合要求并且可以在此引用。有道理吗? - Mark McKenna
C++11仍然支持typedef。此外,您可以通过在DERIVED类型中使用标准构造(例如value_type)来避免第一个示例中的重复int - rubenvb
这个答案实际上并不针对C++11;我提到C++11只是为了说明你可以解决第2个块中的typedef问题。但是我认为第2点是有效的...是的,那可能是做同样事情的更简单的方法。 - Mark McKenna

7

这是我刚刚使用的内容概括而来的。我将其发布出来,因为它是一个非常简单的例子,并且演示了一个实际的用例以及默认参数:

#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;
};

最近我也遇到了这个使用案例,准备编写自己的 STL 兼容容器,但是请参考这个线程和相应的答案,了解标准库实际上为什么不采用这种方法(简而言之——这意味着调用者无法传递一个带有超过一个模板参数的分配器):https://dev59.com/bmct5IYBdhLWcg3wFpm1#18682138 - saxbophone

4

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

假设您想打印容器的每个元素,您可以使用以下代码而不需要模板模板参数:

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)。在前一种情况下,模板将由编译器实例化,编译器会抱怨循环中使用了 c,后者根本不会实例化模板,因为找不到匹配的类型。
一般而言,如果您的模板类/函数旨在处理模板类作为模板参数,则最好明确说明。

我不同意;你的例子是否仅仅通过任意限制使用模板创建的容器的范围来降低了print_container函数的效用?如果有人编写了一个适用于基于范围的for循环的类,但该类的性质只适用于特定类型,他们将无法将其与print_container一起使用,即使该函数的编写方式本来可以正常工作。 - flarn2006

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;
}

3
我用它来处理版本化的类型。
如果您有一个通过模板进行版本控制的类型,比如 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() 函数,该函数从任何旧版本返回最新版本的类型(对于可能存储一段时间但需要使用今天最新工具处理的日志非常有用)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接