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