【问题标题】:What are some uses of template template parameters?模板模板参数有哪些用途?
【发布时间】:2010-09-17 20:16:29
【问题描述】:

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

【问题讨论】:

标签: c++ templates template-templates


【解决方案1】:

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

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

这就是我更喜欢编写这种类型的代码的方式。

【讨论】:

  • 如果 f 是库用户定义的函数,那么用户需要将 std::allocator 作为参数传递是很难看的。我本来希望没有 std::allocator 参数的版本使用 std::vector 的默认参数工作。这个 wrt C++0x 有更新吗?
  • 好吧,你不必提供分配器。重要的是模板模板参数是在正确数量的参数上定义的。但是该函数不应该关心它们的“类型”或含义是什么,以下在 C++98 中运行良好:template&lt;template&lt;class, class&gt; class C, class T, class U&gt; void f(C&lt;T, U&gt; &amp;v)
  • 我想知道为什么实例化是f&lt;vector,int&gt; 而不是f&lt;vector&lt;int&gt;&gt;
  • @bobobobo 这两个意思不同。 f&lt;vector,int&gt; 表示f&lt;ATemplate,AType&gt;f&lt;vector&lt;int&gt;&gt; 表示f&lt;AType&gt;
  • @phaedrus:(很久以后......)好点,改进了示例以使分配器通用并且示例更清晰:-)
【解决方案2】:

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

然后你会发现 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

顺便说一句,使用允许可变参数模板的 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 

【讨论】:

  • 这是模板模板参数的一个很好的例子,因为它展示了每个人都必须处理的案例。
  • 这是C++模板中对我来说最清醒的答案。 @WhozCraig 您是如何获得模板扩展详细信息的?
  • @Arun gcc 支持名为__PRETTY_FUNCTION__ 的宏,除其他外,它以纯文本形式报告模板参数描述。铿锵也这样做。有时是最方便的功能(如您所见)。
  • 这里的template模板参数并没有真正增加任何值。您不妨将常规模板参数用作类模板的任何给定实例。
  • 我必须同意大卫·斯通的观点。这里没有模板模板参数的意义。制作一个普通的模板(模板)会简单得多,而且同样有效。我知道这篇文章已经很老了,所以我只为偶然发现这个答案以寻找有关模板模板信息的人添加我的 2 美分。
【解决方案3】:

以下是 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;

【讨论】:

  • 针对策略模式以外的示例特别要求的问题。
  • 我正是从这本书中得出这个问题的。值得注意的是,模板模板参数也出现在 Typelist 章节和 Class generation with Typelists 章节中。
【解决方案4】:

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

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

【讨论】:

  • 你能用类似“template TT> CLayerT”和“class CLayerCuda: public CLayerT>”来强制推导T吗?如果您不需要 TT
  • 没关系:模板
【解决方案5】:

假设您正在使用 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,除非我还没有学到另一个技巧。

【讨论】:

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

【讨论】:

    【解决方案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 兼容容器,但是请参阅这个线程和相应的答案,了解为什么这不是标准库实际采用的方法(TL; DR——这意味着调用者不可能传递一个带有多个模板参数的分配器):stackoverflow.com/questions/12362363/…
    【解决方案8】:

    在 pfalcon 提供的带有可变参数模板的解决方案中,由于可变参数专业化的贪婪特性,我发现实际上很难将 ostream 运算符专门用于 std::map。这是一个对我有用的小修改:

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

    【讨论】:

      【解决方案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';
      }
      

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

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

      【讨论】:

        【解决方案10】:

        我将它用于版本化类型。

        如果你有一个通过模板版本化的类型,例如MyType&lt;version&gt;,你可以编写一个函数来捕获版本号:

        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&lt;Version&gt; 并返回MyType&lt;Version+1&gt;,甚至递归它们以具有ToNewest() 函数,该函数从任何旧版本返回类型的最新版本(对于可能已存储一段时间但需要使用当今最新工具处理的日志)。

        【讨论】:

          猜你喜欢
          • 2021-07-18
          • 2014-09-08
          • 1970-01-01
          • 1970-01-01
          • 2011-10-01
          • 2014-11-20
          • 1970-01-01
          相关资源
          最近更新 更多