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


Answers:


197

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

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

这里, H是一个模板,但我希望此函数处理的所有特化H

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

我一直在尝试考虑好的例子,说实话,大多数情况下这不是必需的,但让我们来设计一个例子。假装std::vector 没有typedef value_type

那么,您将如何编写一个函数来为vectors元素创建正确类型的变量?这会起作用。

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的默认参数即可工作。此wrt C ++ 0x上有任何更新吗?
艾米特

好吧,您不必提供分配器。重要的是模板模板参数是在正确数量的参数上定义的。但是该函数不必关心它们的“类型”或含义是什么,下面在C ++ 98中可以很好地工作:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon 2013年

我不知道为什么实例化是这样,f<vector,int>而不是f<vector<int>>
bobobobo

2
@bobobobo这两个含义不同。f<vector,int>手段f<ATemplate,AType>f<vector<int>>手段f<AType>
user362515 2014年

@phaedrus:(很久以后...)好点,改善了例如使分配器通用的,这个例子更清楚:-)
埃文·特兰

163

实际上,模板模板参数的用例非常明显。一旦了解到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个模板参数-值类型和分配器。虽然分配器是默认设置,但我们仍应在模板运算符<<中考虑它的存在:

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

瞧,这将自动地适用于所有遵守标准协议的当前和将来的序列容器。要将地图添加到混合中,请参考一下,注意它们接受4个模板参数,因此我们需要上面带有4-arg模板模板参数的operator <<的另一个版本。我们还将看到std:pair尝试使用2-arg operator <<来渲染先前定义的序列类型,因此我们将仅为std :: pair提供一个特殊化。

顺便说一句,使用C + 11可以使用可变参数模板(因此应该允许可变参数模板args),可以使用单个运算符<<将其全部规则。例如:

#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 

9
这是模板模板参数如此出色的示例,因为它显示了每个人都必须处理的情况。
Ravenwater

3
这是C ++模板中对我来说最醒目的答案。@WhozCraig您如何获得模板扩展的详细信息?
阿伦(Arun)2014年

3
@Arun gcc支持名为的宏__PRETTY_FUNCTION__,该宏除其他外以纯文本形式报告模板参数描述。铛也这样做。有时是最方便的功能(如您所见)。
WhozCraig 2014年

20
这里的template template参数并没有真正添加任何值。您也可以将常规模板参数用作类模板的任何给定实例。
大卫·斯通

9
我必须同意戴维·斯通的观点。这里没有指向template template参数。制作普通模板(模板<typename Container>)将更加简单并且同样有效。我知道这篇帖子已经很老了,所以我只为那些偶然发现此答案以寻找有关模板模板信息的人添加2美分。
吉姆·瓦戈

67

这是摘自“现代C ++设计-应用通用编程和设计模式”的简单示例 Andrei Alexandrescu的:

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

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

1
该问题专门要求提供政策模式以外的其他示例。
user2913094

我正是从这本书中提出这个问题的。值得注意的是,模板模板参数也出现在“类型列表”一章和“ 使用类型列表生成类”一章中。
维克多

18

这是我的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的输入,但在CPU(通过指定TT = Tensor)或在GPU(通过指定TT = TensorGPU)上,connection_matrix始终为int。


您是否可以使用类似“模板<class T,模板<T> TT> CLayerT”和“ CLayerCuda类:public CLayerT <TensorGPU <float >>”的类来强制推导T?如果您不需要TT <otherT>
NicoBerrogorry

永远不要:template <template <template T> class U> class B1 {}; 来自ibm.com/support/knowledgecenter/zh-CN/SSLTBW_2.3.0/…来自Google的快速搜索
NicoBerrogorry

12

假设您正在使用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;

不幸的是,对于派生模板的每个实例化,您都需要一个named_interface_type,除非还有我尚未学习的另一种技巧。


我需要一些代码的确切解决方案(谢谢!)。尽管它有效,但我不明白derived没有模板参数的情况下如何使用模板类,即行typedef typename interface<derived, VALUE> type;
Carlton,

@Carlton之所以能够正常工作,是因为将要填充的相应模板参数定义为template <typename>。从某种意义上讲,您可以认为模板参数具有“元类型”;模板参数typename的普通元类型是,它需要用常规类型填充;元template类型意味着它需要填充对模板的引用。 derived定义了一个模板,该模板接受一个元typename类型的参数,因此符合要求,可以在此处引用。合理?
Mark McKenna

C ++ 11仍然typedef。另外,您可以int通过使用标准构造(例如value_typeDERIVED类型的a)来避免第一个示例中的重复项。
rubenvb

这个答案实际上并不针对C ++ 11。我引用C ++ 11只是为了说您可以解决第typedef2块中的问题。但是,我认为第2点是有效的……是的,那可能是做相同事情的一种更简单的方法。
Mark McKenna

7

这是我遇到的:

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

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

2

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

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

2

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

假设您要打印容器的每个元素,则可以使用以下没有模板template参数的代码

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

或带有模板template参数

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 for循环中,后者根本不会实例化模板,因为找不到匹配的类型。

一般而言,如果您将模板类/函数设计为将模板类作为模板参数进行处理,则最好弄清楚这一点。


1

我将其用于版本化类型。

如果您通过诸如的模板对类型进行了版本控制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()从任何较旧版本返回该类型的最新版本的函数(对于以前存储了一段时间的日志非常有用)但需要使用当今最新的工具进行处理)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.