Які функції використання параметрів шаблону шаблону?


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. Чи є якісь оновлення на цьому Wrt 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>>.
bobobobo

2
@bobobobo Ці два значення означають різні речі. f<vector,int>засоби f<ATemplate,AType>, f<vector<int>>засобиf<AType>
користувач362515

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

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 

9
Це настільки приємний приклад параметрів шаблону шаблону, оскільки він показує випадок, з яким усі мали справу.
Ravenwater

3
Це найбільш пробуджуюча для мене відповідь у шаблонах C ++. @WhozCraig Як ви отримали детальну інформацію про розширення шаблону?
Арун

3
@Arun gcc підтримує макрос __PRETTY_FUNCTION__, який називається , який, серед іншого, повідомляє про описи параметрів шаблону у простому тексті. Кланг робить це також. Іноді найзручніша функція (як бачите).
WhozCraig

20
Параметр шаблону шаблону тут насправді не додає жодного значення. Ви можете просто використовувати параметр звичайного шаблону, як і будь-який даний примірник шаблону класу.
Девід Стоун

9
Я повинен погодитися з Девідом Стоун. Тут немає сенсу до параметра шаблону шаблону. Було б набагато простіше і не менш ефективно зробити звичайний шаблон (шаблон <typename Container>). Я знаю, що ця публікація досить стара, тому я лише додаю свої 2 центи для людей, які натрапляють на цю відповідь, шукаючи інформацію про шаблони шаблонів.
Джим Варго

67

Ось простий приклад, взятий із "Сучасного дизайну 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;

1
Питання, зокрема, було запропоновано для інших прикладів, ніж для політики.
користувач2913094

Я прийшов до цього питання саме з цієї книги. Варто зауважити, що параметри шаблону шаблону також відображаються в розділі Typelist та генерації Class із розділом Typelists .
Віктор

18

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


Чи можете ви примусити вирахувати T таким чином: "шаблон <клас T, шаблон <T> TT> CLayerT" і "клас CLayerCuda: public CLayerT <TensorGPU <flolo>>"? У випадку, якщо вам не знадобився TT <otherT>
NicoBerrogorry

НІКОЛИ НЕ РОБИТИ: шаблон <шаблон <клас T> клас U> клас В1 {}; від 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 не працює, тому що ви не можете ввести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;
Карлтон,

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

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

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


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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.