Я видел несколько примеров C ++, использующих параметры шаблонов шаблонов (то есть шаблоны, которые принимают шаблоны в качестве параметров) для разработки классов на основе политик. Какие другие применения у этой техники?
Я видел несколько примеров C ++, использующих параметры шаблонов шаблонов (то есть шаблоны, которые принимают шаблоны в качестве параметров) для разработки классов на основе политик. Какие другие применения у этой техники?
Ответы:
Я думаю, вам нужно использовать синтаксис шаблона шаблона для передачи параметра, тип которого является шаблоном, зависящим от другого шаблона, например:
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;
}
именно так я бы предпочел написать этот тип кода.
template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
f<vector,int>
и нет f<vector<int>>
.
f<vector,int>
значит f<ATemplate,AType>
, f<vector<int>>
значитf<AType>
На самом деле, сценарий использования параметров шаблона шаблона довольно очевиден. Как только вы узнаете, что в 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
__PRETTY_FUNCTION__
, который, помимо прочего, сообщает об описании параметров шаблона в виде простого текста. Clang делает это также. Иногда очень удобная функция (как вы можете видеть).
Вот простой пример, взятый из « Андрея Александреску»: «Современный дизайн 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;
Вот еще один практический пример из моей библиотеки 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).
Допустим, вы используете 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;
template <typename>
. В некотором смысле вы можете думать о параметрах шаблона как о «метатипе»; нормальный метатип для параметра шаблона typename
означает, что он должен быть заполнен обычным типом; то template
метатип средство он должен быть заполнено ссылкой на шаблон. derived
определяет шаблон, который принимает один typename
метатипизированный параметр, поэтому он соответствует требованиям и на него можно ссылаться здесь. Есть смысл?
typedef
. Кроме того, вы можете избежать дублирования int
в вашем первом примере, используя стандартную конструкцию, например, value_type
в типе DERIVED.
typedef
проблему из блока 2. Но пункт 2 действителен, я думаю ... да, возможно, это был бы более простой способ сделать то же самое.
Вот с чем я столкнулся:
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();
}
В решении с вариадическими шаблонами, предоставленными 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;
}
Вот один обобщенный из того, что я только что использовал. Я публикую его, так как это очень простой пример, и он демонстрирует практический пример использования вместе с аргументами по умолчанию:
#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;
};
Это улучшает читабельность вашего кода, обеспечивает дополнительную безопасность типов и экономит некоторые усилия компилятора.
Скажем, вы хотите напечатать каждый элемент контейнера, вы можете использовать следующий код без параметра шаблона шаблона
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, последний не будет создавать экземпляр шаблона вообще, так как не может быть найден соответствующий тип.
Вообще говоря, если ваш класс / функция шаблона предназначен для обработки класса шаблона в качестве параметра шаблона, лучше прояснить это.
Я использую это для версионных типов.
Если у вас есть тип версионный через шаблон, например 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()
функцию, которая возвращает последнюю версию типа из любой более старой версии (очень полезно для журналов, которые могли быть сохранены некоторое время назад). но должны быть обработаны с помощью новейшего инструмента сегодня).