Какое использование параметров шаблона шаблона?

238

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

Ферруччо
источник
4
Я пришел с другого направления (FP, Haskell и т. Д.) И приземлился на это: stackoverflow.com/questions/2565097/higher-kinded-types-with-c
Эрик Каплун,

Ответы:

197

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

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

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

Эван Теран
источник
1
Если f является функцией, определенной пользователем библиотеки, уродливо, что пользователь должен передать std :: allocator <T> в качестве аргумента. Я ожидал, что версия без аргумента std :: allocator работала с использованием параметра по умолчанию std :: vector. Есть ли какие-либо обновления на этом C ++ 0x?
Амит
Ну, вам не нужно предоставлять распределитель. Важным является то, что параметр шаблона шаблона был определен для правильного числа аргументов. Но функция не должна заботиться о том, что их "типы" или значение, следующие хорошо работает в 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>>.
Бобобобо
2
@bobobobo Это две разные вещи. f<vector,int>значит f<ATemplate,AType>, f<vector<int>>значитf<AType>
user362515
@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;
}

Тогда вы поймете, что код для вектора такой же, для 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-аргументным шаблоном param. Мы также увидим, что std: pair пытается отображаться с помощью оператора 2-arg << для типов последовательностей, которые мы определили ранее, поэтому мы предоставим специализацию только для std :: pair.

Между прочим, с 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 
pfalcon
источник
9
Это такой приятный пример параметров шаблона шаблона, поскольку он показывает случай, с которым всем пришлось иметь дело.
Равенуотер
3
Это самый пробуждающий ответ для меня в шаблонах C ++. @WhozCraig Как вы узнали подробности расширения шаблона?
Арун
3
@Arun gcc поддерживает макрос __PRETTY_FUNCTION__, который, помимо прочего, сообщает об описании параметров шаблона в виде простого текста. Clang делает это также. Иногда очень удобная функция (как вы можете видеть).
WhozCraig
20
Параметр шаблона шаблона здесь не добавляет никакого значения. Вы можете также использовать обычный параметр шаблона как любой заданный экземпляр шаблона класса.
Дэвид Стоун
9
Я должен согласиться с Дэвидом Стоуном. Здесь нет никакого смысла в параметре шаблона шаблона. Было бы намного проще и одинаково эффективно создать простой шаблон (template <typename Container>). Я знаю, что этот пост довольно старый, поэтому я добавляю свои 2 цента только для тех, кто наткнулся на этот ответ и ищет информацию о шаблонах.
Джим Варго
67

Вот простой пример, взятый из « Андрея Александреску»: «Современный дизайн 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;
yoav.aviram
источник
1
Вопрос специально задан для примеров, отличных от модели политики.
user2913094
Я пришел к этому вопросу именно из этой книги. Стоит отметить, что параметры шаблона шаблона также появляются в главе «Typelist» и в главе «Создание класса с помощью Typelists» .
Виктор
18

Вот еще один практический пример из моей библиотеки 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).

Михаил Сиротенко
источник
Можете ли вы заставить удержание T с помощью чего-то вроде: "template <class T, template <T> TT> CLayerT" и "class CLayerCuda: public CLayerT <TensorGPU <float >>"? В случае, если вам не нужен TT <otherT>
NicoBerrogorry
НИКОГДА НЕ УЗНАЙТЕ: template <template <class T> class U> class B1 {}; от ibm.com/support/knowledgecenter/en/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;

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

Это также позволяет вам создавать 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;

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

Марк МакКенна
источник
Мне нужно было это точное решение для некоторого кода (спасибо!). Хотя это работает, я не понимаю, как класс шаблона derivedможет использоваться без аргументов шаблона, то есть строкиtypedef typename interface<derived, VALUE> type;
Carlton
@Carlton работает в основном потому, что соответствующий заполняемый параметр шаблона определяется как template <typename>. В некотором смысле вы можете думать о параметрах шаблона как о «метатипе»; нормальный метатип для параметра шаблона typenameозначает, что он должен быть заполнен обычным типом; то templateметатип средство он должен быть заполнено ссылкой на шаблон. derivedопределяет шаблон, который принимает один typenameметатипизированный параметр, поэтому он соответствует требованиям и на него можно ссылаться здесь. Есть смысл?
Марк МакКенна
C ++ 11 пока еще typedef. Кроме того, вы можете избежать дублирования intв вашем первом примере, используя стандартную конструкцию, например, value_typeв типе DERIVED.
rubenvb
Этот ответ на самом деле не нацелен на C ++ 11; Я ссылался на C ++ 11 только для того, чтобы сказать, что вы можете обойти typedefпроблему из блока 2. Но пункт 2 действителен, я думаю ... да, возможно, это был бы более простой способ сделать то же самое.
Марк МакКенна
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, мне было трудно фактически специализировать 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

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

#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;
};
imallett
источник
2

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

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

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

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

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

cd127
источник