Я бачив кілька прикладів 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 аргументи шаблону - для типу значень та розподільника. Незважаючи на те, що для алокатора встановлено дефолт, ми все одно повинні враховувати його існування в нашому операторі шаблонів <<:
template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...
Voila, яка буде працювати автоматично для всіх теперішніх та майбутніх контейнерів послідовностей, що дотримуються стандартного протоколу. Щоб додати карти до суміші, потрібно поглянути за посиланням на те, що вони приймають 4 параметри шаблону, тому нам знадобиться інша версія оператора << вище з парам-шаблоном шаблону 4-аргументів. Ми також побачили, що std: пара намагається винести з 2-arg оператором << для типів послідовностей, які ми визначили раніше, тож ми б забезпечили спеціалізацію саме для std :: pair.
Btw, з 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__, який називається , який, серед іншого, повідомляє про описи параметрів шаблону у простому тексті. Кланг робить це також. Іноді найзручніша функція (як бачите).
Ось простий приклад, взятий із "Сучасного дизайну C ++ - загальне моделювання програмування та дизайну" Андрія Олександреску:
Він використовує класи з параметрами шаблону шаблону, щоб реалізувати шаблон політики:
// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
Він пояснює: Зазвичай клас хосту вже знає або може легко вивести шаблонний аргумент класу policy. У наведеному вище прикладі WidgetManager завжди керує об'єктами типу Widget, тому вимагає від користувача знову вказувати Widget при створенні CreationPolicy зайвим і потенційно небезпечним. У цьому випадку бібліотечний код може використовувати параметри шаблону шаблону для визначення політики.
Ефект полягає в тому, що клієнтський код може використовувати "WidgetManager" в більш елегантному вигляді:
typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;
Замість більш громіздкого та схильного до помилок способу, який потребував би визначення аргументів шаблону шаблону:
typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
Ось ще один практичний приклад з моєї бібліотеки CUNA Convolutional нейронної мережі . У мене є такий шаблон класу:
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>
який матиме як ваги, так і входи типу float та GPU, але 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 не працює, тому що ви не можете ввестиdedef до не визначеного шаблону. Однак це працює (і 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можна використовувати без аргументів шаблону, тобто рядка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()функцію, яка повертає останню версію типу з будь-якої старої версії (дуже корисно для журналів, які, можливо, були збережені деякий час назад але їх потрібно обробити за допомогою новітнього інструменту сьогодні).