Какое использование параметров шаблона шаблона? - PullRequest
207 голосов
/ 18 октября 2008

Я видел несколько примеров того, как C ++ использует параметры шаблона шаблона (то есть шаблоны, которые принимают шаблоны в качестве параметров) для разработки классов на основе политик. Какие другие применения у этой техники?

Ответы [ 9 ]

172 голосов
/ 18 октября 2008

Я думаю, что вам нужно использовать синтаксис шаблона шаблона для передачи параметра, тип которого является шаблоном, зависящим от другого шаблона, например:

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

Здесь H - шаблон, но я хотел, чтобы эта функция работала со всеми специализациями H.

ПРИМЕЧАНИЕ : Я программировал на С ++ много лет и нуждался в этом только один раз. Я считаю, что это редко необходимая функция (конечно, удобная, когда она вам нужна!).

Я пытался придумать хорошие примеры, и, честно говоря, большую часть времени в этом нет необходимости, но давайте создадим пример. Давайте представим, что 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;
}

именно так я бы предпочел написать код такого типа.

142 голосов
/ 14 января 2013

На самом деле, сценарий использования параметров шаблона шаблона довольно очевиден. Как только вы узнаете, что в 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, мы все равно должны учитывать его существование в нашем шаблонном операторе <<: </p>

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

Вуаля, которая будет работать автоматически для всех существующих и будущих контейнеров последовательностей, придерживающихся стандартного протокола. Чтобы добавить карты в микс, нужно взглянуть на ссылку, чтобы заметить, что они принимают 4 параметра шаблона, поэтому нам понадобится другая версия оператора << выше с 4-аргументным шаблоном param. Мы также увидим, что std: pair пытается отображаться с помощью оператора 2-arg << для типов последовательностей, которые мы определили ранее, поэтому мы предоставим специализацию только для std :: pair. </p>

Кстати, с C + 11, который допускает шаблоны с переменным числом (и, следовательно, должен разрешать аргументы шаблона с переменным числом элементов), можно было бы иметь один оператор <<, чтобы управлять ими всеми. Например: </p>

#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 
60 голосов
/ 18 октября 2008

Вот простой пример, взятый из 'Современный дизайн C ++ - применены общие шаблоны программирования и проектирования' от Андрея Александреску:

Он использует классы с параметрами шаблона шаблона для реализации шаблона политики:

// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
   ...
};

Он объясняет: Обычно хост-класс уже знает или может легко вывести аргумент шаблона класса политики. В приведенном выше примере WidgetManager всегда управляет объектами типа Widget, поэтому требование пользователя снова указывать Widget в экземпляре CreationPolicy является избыточным и потенциально опасным. В этом случае код библиотеки может использовать параметры шаблона шаблона для указания политик.

В результате клиентский код может использовать 'WidgetManager' более элегантным способом:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

Вместо более обременительного и подверженного ошибкам способа, который потребовалось бы для определения, в котором отсутствуют аргументы шаблона:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
18 голосов
/ 17 июля 2011

Вот еще один практический пример из моей CUDA Convolutional нейронной библиотеки . У меня есть следующий шаблон класса:

template <class T> class Tensor

, который фактически реализует манипулирование n-мерными матрицами. Также есть шаблон дочернего класса:

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

, которая реализует ту же функциональность, но в графическом процессоре. Оба шаблона могут работать со всеми основными типами, такими как 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 и для графического процессора, но connection_matrix всегда будет иметь значение int, либо на процессоре (указав TT = Tensor), либо на GPU (указав TT = TensorGPU).

10 голосов
/ 09 октября 2012

Допустим, вы используете 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;

Обратите внимание, что вы исключаете непосредственное предоставление других параметров шаблона для шаблона производного ; «интерфейс» все еще получает их.

Это также позволяет вам создавать typedefs в «интерфейсе», которые зависят от параметров типа, которые будут доступны из производного шаблона.

Приведенный выше typedef не работает, потому что вы не можете typedef для неопределенного шаблона. Это работает, однако (и C ++ 11 имеет встроенную поддержку шаблонов typedefs):

template <typename VALUE>
struct derived_interface_type {
    typedef typename interface<derived, VALUE> type;
};

typedef typename derived_interface_type<int>::type derived_t;

К сожалению, вам нужен один производный_интерфейс_типа для каждого экземпляра производного шаблона, если только нет другого трюка, который я еще не изучил.

6 голосов
/ 29 мая 2014

Вот с чем я столкнулся:

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 голосов
/ 19 февраля 2015

В решении с вариадическими шаблонами, предоставленными 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;
}
2 голосов
/ 20 мая 2015

Вот один обобщенный из того, что я только что использовал. Я публикую его, поскольку это очень простой пример, и он демонстрирует практический пример использования вместе с аргументами по умолчанию:

#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 голосов
/ 30 августа 2017

Улучшает читабельность вашего кода, обеспечивает дополнительную безопасность типов и экономит некоторые усилия компилятора.

Скажем, вы хотите напечатать каждый элемент контейнера, вы можете использовать следующий код без параметра шаблона шаблона

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 в цикле for, а последний не будет создавать экземпляр шаблона вообще, так как соответствующий тип не может быть найден.

Вообще говоря, если ваш класс / функция шаблона предназначен для обработки класса шаблона в качестве параметра шаблона, лучше прояснить это.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...