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

What are some uses of template template parameters?

提问人:Ferruccio 提问时间:10/18/2008 最后编辑:Rakete1111Ferruccio 更新时间:10/18/2019 访问量:226128

问:

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

C++ 模板 template-templates

评论

6赞 Erik Kaplun 3/2/2014
我从另一个方向(FP、Haskell 等)来到这里:stackoverflow.com/questions/2565097/higher-kinded-types-with-c

答:

270赞 Evan Teran 10/18/2008 #1

我认为您需要使用模板模板语法来传递一个参数,该参数的类型是依赖于另一个模板的模板,如下所示:

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

这里,是一个模板,但我希望这个函数处理 .HH

注意:我从事 c++ 编程多年,只需要一次。我发现这是一个很少需要的功能(当然,当您需要它时很方便!

我一直在努力想出好的例子,老实说,大多数时候这不是必需的,但让我们举个例子。让我们假装没有 .std::vectortypedef 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;
}

注意:有两个模板参数,type 和 allocator,因此我们必须同时接受它们。幸运的是,由于类型推导,我们不需要显式写出确切的类型。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赞 amit kumar 1/13/2011
如果 f 是库用户定义的函数,那么用户需要将 std::allocator<T> 作为参数传递是丑陋的。我本来以为没有 std::allocator 参数的版本可以使用 std::vector 的默认参数工作。这个 wrt C++0x 有什么更新吗?
1赞 pfalcon 1/14/2013
好吧,您不必提供分配器。重要的是,模板模板参数是在正确数量的参数上定义的。但是该函数不应该关心它们的“类型”或含义是什么,以下在 C++98 中效果很好:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
0赞 bobobobo 12/18/2013
我想知道为什么实例化是而不是.f<vector,int>f<vector<int>>
2赞 user362515 6/13/2014
@bobobobo 这两者的含义不同。 手段 , 手段f<vector,int>f<ATemplate,AType>f<vector<int>>f<AType>
0赞 Evan Teran 8/3/2015
@phaedrus:(很久以后...)好点,改进了示例,使分配器通用,示例更清晰:-)
78赞 yoav.aviram 10/18/2008 #2

下面是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 2/12/2015
该问题特别要求提供政策模式以外的例子。
0赞 Victor 1/30/2020
我正是从这本书中得出了这个问题。值得一提的是,模板模板参数也出现在“类型列表”一章和“使用类型列表生成类”一章中。
22赞 Mikhail Sirotenko 7/18/2011 #3

这是我的 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>

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

评论

0赞 NicoBerrogorry 4/18/2018
你能用这样的内容强制扣除 T 吗:“模板<类 T,模板 <T> TT> CLayerT”和“类 CLayerCuda:公共 CLayerT<TensorGPU<float>>”?如果您不需要 TT<otherT>
0赞 NicoBerrogorry 4/19/2018
没关系:template<template<class T> class U>class B1 { };来自 ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/... 来自快速的谷歌搜索
13赞 Mark McKenna 10/10/2012 #4

假设您正在使用 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 对模板类型定义具有本机支持):

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,除非还有另一个我还没有学到的技巧。

评论

0赞 Carlton 1/27/2018
我需要这个确切的解决方案来编写一些代码(谢谢!虽然它有效,但我不明白如何在没有模板参数的情况下使用模板类,即行derivedtypedef typename interface<derived, VALUE> type;
0赞 Mark McKenna 1/28/2018
@Carlton它之所以有效,主要是因为要填充的相应模板参数被定义为 .从某种意义上说,您可以将模板参数视为具有“元类型”;模板参数的正常元类型是,这意味着它需要由常规类型填充;元类型意味着它需要填充对模板的引用。 定义一个接受一个元类型化参数的模板,因此它符合要求,可以在此处引用。有意义?template <typename>typenametemplatederivedtypename
0赞 rubenvb 6/19/2018
C++11 仍然.此外,还可以通过使用标准构造(如 DERIVED 类型中的 a)来避免第一个示例中的重复。typedefintvalue_type
0赞 Mark McKenna 6/20/2018
这个答案实际上并不针对 C++11;我引用 C++11 只是为了说您可以从块 2 中解决问题。但我认为第 2 点是有效的......是的,这可能是做同样事情的更简单的方法。typedef
197赞 pfalcon 1/14/2013 #5

实际上,模板模板参数的用例相当明显。一旦你了解到 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;
}

然后你就会发现矢量的代码是一样的,因为forward_list是一样的,实际上,即使对于多种地图类型,它仍然是一样的。除了元接口/协议之外,这些模板类没有任何共同点,使用模板模板参数可以捕获所有这些模板类的共性。但是,在继续编写模板之前,值得检查一下引用,以回想序列容器接受 2 个模板参数 - 值类型和分配器。虽然 allocator 是默认的,但我们仍然应该在模板运算符中考虑它的存在<<:

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

瞧,这将自动适用于所有遵守标准协议的当前和未来序列容器。要将映射添加到组合中,需要查看引用以注意它们接受 4 个模板参数,因此我们需要另一个版本的运算符<<上面带有 4-arg 模板模板参数。我们还会看到,对于我们之前定义的序列类型<<std:pair 尝试使用 2-arg 运算符进行渲染,因此我们将仅为 std::p air 提供专用化。

顺便说一句,使用 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 

评论

20赞 Ravenwater 2/4/2013
这是模板模板参数的一个甜蜜示例,因为它显示了每个人都必须处理的案例。
4赞 Arun 9/14/2014
这是我在C++模板中最醒悟的答案。@WhozCraig 您是如何获得模板扩展详细信息的?
4赞 WhozCraig 9/14/2014
@Arun GCC 支持一个名为 的宏,除其他外,该宏以纯文本形式报告模板参数描述。Clang也这样做。有时是最方便的功能(如您所见)。__PRETTY_FUNCTION__
26赞 David Stone 10/5/2015
此处的模板模板参数并没有真正增加任何值。您不妨只使用常规模板参数作为类模板的任何给定实例。
13赞 Jim Vargo 7/14/2016
我必须同意大卫·斯通的观点。此处的模板模板参数没有意义。制作一个普通模板(模板<typename Container>)会简单得多,也同样有效。我知道这篇文章已经很老了,所以我只为那些偶然发现这个答案的人添加我的 2 美分,以寻找有关模板模板的信息。
14赞 Cookie 5/29/2014 #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();
}
4赞 Kuberan Naganathan 2/19/2015 #7

在 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赞 geometrian 5/20/2015 #8

这是我刚刚使用的东西的一个概括。我之所以发布它,是因为这是一个非常简单的示例,它演示了一个实际用例以及默认参数:

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

评论

0赞 saxbophone 8/9/2020
我最近也遇到了这个用例,准备编写我自己的 STL 兼容容器,但请参阅此线程和相应的答案,了解为什么这不是标准库实际采用的方法(TL;DR —这意味着调用方无法传递采用多个模板参数的分配器):stackoverflow.com/questions/12362363/...
4赞 colin 8/31/2017 #9

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

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

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

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

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

评论

0赞 Sparkette 10/24/2022
我不同意;您的示例是否只是通过任意将其范围限制为使用模板创建的容器来降低函数的效用?如果有人要编写一个在基于范围的循环中工作的类,但该类的性质只对特定类型有意义,他们将无法将其与 一起使用,即使该函数的编写方式本来可以正常工作。print_containerforprint_container
3赞 cd127 9/30/2019 #10

我将其用于版本化类型。

如果您有一个通过模板(如 )进行版本控制的类型,则可以编写一个函数,在其中可以捕获版本号: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()