Quelles sont les utilisations des paramètres de modèle de modèle?

238

J'ai vu quelques exemples de C ++ utilisant des paramètres de modèle de modèle (c'est-à-dire des modèles qui prennent des modèles comme paramètres) pour effectuer une conception de classe basée sur des règles. Quelles sont les autres utilisations de cette technique?

Ferruccio
la source
4
Je suis venu de l'autre direction (FP, Haskell, etc.) et j'ai atterri sur ceci: stackoverflow.com/questions/2565097/higher-kinded-types-with-c
Erik Kaplun

Réponses:

197

Je pense que vous devez utiliser la syntaxe du modèle de modèle pour passer un paramètre dont le type est un modèle dépendant d'un autre modèle comme celui-ci:

template <template<class> class H, class S>
void f(const H<S> &value) {
}

Voici Hun modèle, mais je voulais que cette fonction traite de toutes les spécialisations deH .

REMARQUE : je programme c ++ depuis de nombreuses années et je n'en ai eu besoin qu'une seule fois. Je trouve que c'est une fonctionnalité rarement nécessaire (bien sûr pratique quand vous en avez besoin!).

J'ai essayé de penser à de bons exemples, et pour être honnête, la plupart du temps ce n'est pas nécessaire, mais inventons un exemple. Faisons semblant que std::vector ne pas avoir untypedef value_type .

Alors, comment écririez-vous une fonction qui peut créer des variables du bon type pour les éléments vecteurs? Cela fonctionnerait.

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

REMARQUE : std::vectora deux paramètres de modèle, type et allocateur, nous avons donc dû les accepter tous les deux. Heureusement, en raison de la déduction de type, nous n'aurons pas besoin d'écrire explicitement le type exact.

que vous pouvez utiliser comme ceci:

f<std::vector, int>(v); // v is of type std::vector<int> using any allocator

ou mieux encore, nous pouvons simplement utiliser:

f(v); // everything is deduced, f can deal with a vector of any type!

MISE À JOUR : Même cet exemple artificiel, bien qu'illustratif, n'est plus un exemple étonnant en raison de l'introduction de c ++ 11 auto. Maintenant, la même fonction peut s'écrire:

template <class Cont>
void f(Cont &v) {

    auto temp = v.back();
    v.pop_back();
    // Do some work on temp

    std::cout << temp << std::endl;
}

c'est ainsi que je préférerais écrire ce type de code.

Evan Teran
la source
1
Si f est une fonction définie par l'utilisateur d'une bibliothèque, il est moche que l'utilisateur ait besoin de passer std :: allocator <T> comme argument. Je m'attendais à ce que la version sans l'argument std :: allocator ait fonctionné en utilisant le paramètre par défaut de std :: vector. Y a-t-il des mises à jour sur ce wrt C ++ 0x?
Amit
Eh bien, vous n'avez pas à fournir d'allocateur. Ce qui est important, c'est que le paramètre de modèle de modèle a été défini sur un nombre correct d'arguments. Mais la fonction ne devrait pas se soucier de leurs «types» ou de leur signification, ce qui suit fonctionne bien en C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon
Je me demande pourquoi l'instanciation est f<vector,int>et non f<vector<int>>.
bobobobo
2
@bobobobo Ces deux signifient des choses différentes. f<vector,int>moyens f<ATemplate,AType>, f<vector<int>>moyensf<AType>
user362515
@phaedrus: (beaucoup plus tard ...) de bons points, amélioré l'exemple pour rendre l'allocateur générique et l'exemple plus clair :-)
Evan Teran
163

En fait, l'utilisation des paramètres de modèle de modèle est plutôt évidente. Une fois que vous apprenez que stdlib C ++ a un trou béant de ne pas définir d'opérateurs de sortie de flux pour les types de conteneurs standard, vous devez écrire quelque chose comme:

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

Ensuite, vous comprendrez que le code pour le vecteur est exactement le même, car forward_list est le même, en fait, même pour une multitude de types de cartes, c'est toujours le même. Ces classes de modèles n'ont rien en commun, à l'exception de la méta-interface / du protocole, et l'utilisation du paramètre de modèle de modèle permet de capturer la similitude dans chacun d'eux. Avant de procéder à l'écriture d'un modèle, il convient de vérifier une référence pour rappeler que les conteneurs de séquence acceptent 2 arguments de modèle - pour le type de valeur et l'allocateur. Bien que l'allocateur soit par défaut, nous devons toujours tenir compte de son existence dans notre opérateur de modèle <<:

template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...

Voila, cela fonctionnera automatiquement pour tous les conteneurs de séquence actuels et futurs adhérant au protocole standard. Pour ajouter des cartes au mélange, il faudrait jeter un œil à la référence pour noter qu'elles acceptent 4 paramètres de modèle, nous aurions donc besoin d'une autre version de l'opérateur << ci-dessus avec le paramètre de modèle de modèle à 4 arguments. Nous verrions également que std: pair essaie d'être rendu avec l'opérateur 2-arg << pour les types de séquence que nous avons définis précédemment, donc nous fournirions une spécialisation juste pour std :: pair.

De plus, avec C + 11 qui autorise les modèles variadiques (et devrait donc autoriser les arguments de modèle de modèle variadique), il serait possible d'avoir un seul opérateur << pour les gouverner tous. Par exemple:

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

Production

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
la source
9
Ceci est un exemple si doux de paramètres de modèle de modèle, car il montre un cas auquel tout le monde a dû faire face.
Ravenwater
3
C'est la réponse la plus éveillée pour moi dans les modèles C ++. @WhozCraig Comment avez-vous obtenu les détails de l'extension du modèle?
Arun
3
@Arun gcc prend en charge une macro appelée __PRETTY_FUNCTION__, qui, entre autres, rapporte les descriptions des paramètres du modèle en texte brut. clang le fait aussi. Une fonctionnalité très pratique parfois (comme vous pouvez le voir).
WhozCraig
20
Le paramètre de modèle de modèle ici n'ajoute pas vraiment de valeur. Vous pourriez tout aussi bien utiliser un paramètre de modèle standard que n'importe quelle instance donnée d'un modèle de classe.
David Stone
9
Je dois être d'accord avec David Stone. Il n'y a aucun intérêt au paramètre de modèle de modèle ici. Il serait beaucoup plus simple et tout aussi efficace de créer un modèle simple (modèle <conteneur de nom>). Je sais que ce message est assez ancien, donc j'ajoute seulement mes 2 cents pour les personnes qui tombent sur cette réponse à la recherche d'informations sur les modèles de modèles.
Jim Vargo
67

Voici un exemple simple tiré de 'Modern C ++ Design - Generic Programming and Design Patterns Applied' par Andrei Alexandrescu:

Il utilise une classe avec des paramètres de modèle de modèle afin d'implémenter le modèle de politique:

// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
   ...
};

Il explique: En règle générale, la classe hôte connaît déjà, ou peut facilement en déduire, l'argument modèle de la classe de règles. Dans l'exemple ci-dessus, WidgetManager gère toujours les objets de type Widget, donc demander à l'utilisateur de spécifier à nouveau Widget dans l'instanciation de CreationPolicy est redondant et potentiellement dangereux.Dans ce cas, le code de bibliothèque peut utiliser des paramètres de modèle de modèle pour spécifier des politiques.

L'effet est que le code client peut utiliser 'WidgetManager' d'une manière plus élégante:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

Au lieu de la manière la plus lourde et la plus sujette aux erreurs qu'une définition manquant d'arguments de modèle de modèle aurait nécessité:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
yoav.aviram
la source
1
La question demandait spécifiquement des exemples autres que le modèle de politique.
user2913094
Je suis venu à cette question exactement de ce livre. Une note digne est que les paramètres de modèle de modèle apparaissent également dans le chapitre Typelist et le chapitre Génération de classe avec Typelists .
Victor
18

Voici un autre exemple pratique de ma bibliothèque de réseaux de neurones convolutionnels CUDA . J'ai le modèle de classe suivant:

template <class T> class Tensor

qui implémente en fait la manipulation de matrices à n dimensions. Il existe également un modèle de classe enfant:

template <class T> class TensorGPU : public Tensor<T>

qui implémente la même fonctionnalité mais en GPU. Les deux modèles peuvent fonctionner avec tous les types de base, comme float, double, int, etc. Et j'ai également un modèle de classe (simplifié):

template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> >
{
    TT<T> weights;
    TT<T> inputs;
    TT<int> connection_matrix;
}

La raison ici d'avoir une syntaxe de modèle de modèle est parce que je peux déclarer l'implémentation de la classe

class CLayerCuda: public CLayerT<TensorGPU, float>

qui aura à la fois des poids et des entrées de type float et sur GPU, mais connection_matrix sera toujours int, soit sur CPU (en spécifiant TT = Tensor) soit sur GPU (en spécifiant TT = TensorGPU).

Mikhail Sirotenko
la source
Pouvez-vous forcer la déduction de T avec quelque chose comme: "modèle <classe T, modèle <T> TT> CLayerT" et "classe CLayerCuda: public CLayerT <TensorGPU <float>>"? Dans le cas où vous n'aviez pas besoin d'un TT <otherT>
NicoBerrogorry
NEVER MIND: modèle <modèle <classe T> classe U> classe B1 {}; sur ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/… à partir d'une recherche rapide sur Google
NicoBerrogorry
12

Supposons que vous utilisez CRTP pour fournir une "interface" pour un ensemble de modèles enfants; et le parent et l'enfant sont paramétriques dans d'autres arguments de modèle:

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;

Notez la duplication de 'int', qui est en fait le même paramètre de type spécifié pour les deux modèles. Vous pouvez utiliser un modèle de modèle pour DERIVED pour éviter cette duplication:

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;

Notez que vous éliminez la fourniture directe des autres paramètres de modèle au modèle dérivé ; "l'interface" les reçoit toujours.

Cela vous permet également de créer des typedefs dans l '"interface" qui dépendent des paramètres de type, qui seront accessibles à partir du modèle dérivé.

Le typedef ci-dessus ne fonctionne pas car vous ne pouvez pas typedef vers un modèle non spécifié. Cela fonctionne cependant (et C ++ 11 a un support natif pour les typedefs de modèle):

template <typename VALUE>
struct derived_interface_type {
    typedef typename interface<derived, VALUE> type;
};

typedef typename derived_interface_type<int>::type derived_t;

Vous avez malheureusement besoin d'un dérivé_interface_type pour chaque instanciation du modèle dérivé, sauf s'il y a une autre astuce que je n'ai pas encore apprise.

Mark McKenna
la source
J'avais besoin de cette solution exacte pour du code (merci!). Bien que cela fonctionne, je ne comprends pas comment la classe de modèle derivedpeut être utilisée sans ses arguments de modèle, c'est-à-dire la lignetypedef typename interface<derived, VALUE> type;
Carlton
@Carlton, cela fonctionne essentiellement parce que le paramètre de modèle correspondant rempli est défini comme un template <typename>. Dans un sens, vous pouvez considérer les paramètres du modèle comme ayant un «métatype»; le métatype normal pour un paramètre de modèle est typenamece qui signifie qu'il doit être rempli par un type régulier; le templatemétatype signifie qu'il doit être rempli avec une référence à un modèle. deriveddéfinit un modèle qui accepte un typenameparamètre métatypé, il correspond donc à la facture et peut être référencé ici. Ça a du sens?
Mark McKenna
C ++ 11 encore typedef. En outre, vous pouvez éviter le doublon intdans votre premier exemple en utilisant une construction standard telle que value_typedans le type DERIVED.
rubenvb
Cette réponse ne cible pas réellement C ++ 11; J'ai référencé C ++ 11 juste pour dire que vous pouvez contourner le typedefproblème à partir du bloc 2. Mais le point 2 est valide, je pense ... oui, ce serait probablement un moyen plus simple de faire la même chose.
Mark McKenna
7

Voici ce que j'ai rencontré:

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)>>>>>>>>
{

};

Peut être résolu pour:

template<class A>
class B
{
  A& a;
};

template< template<class> class B>
class A
{
  B<A> b;
};

class AInstance : A<B> //happy
{

};

ou (code de travail):

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();
}
Biscuit
la source
4

Dans la solution avec les modèles variadic fournis par pfalcon, j'ai trouvé difficile de spécialiser réellement l'opérateur ostream pour std :: map en raison de la nature gourmande de la spécialisation variadique. Voici une légère révision qui a fonctionné pour moi:

#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;
}
Kuberan Naganathan
la source
2

En voici une généralisée à partir de quelque chose que je viens d'utiliser. Je le poste car c'est un exemple très simple, et il montre un cas d'utilisation pratique avec des arguments par défaut:

#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
la source
2

Il améliore la lisibilité de votre code, offre une sécurité de type supplémentaire et économise certains efforts du compilateur.

Supposons que vous souhaitiez imprimer chaque élément d'un conteneur, vous pouvez utiliser le code suivant sans paramètre de modèle de modèle

template <typename T> void print_container(const T& c)
{
    for (const auto& v : c)
    {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}

ou avec un paramètre de modèle de modèle

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

Supposons que vous transmettiez un entier print_container(3). Pour le premier cas, le modèle sera instancié par le compilateur qui se plaindra de l'utilisation de cdans la boucle for, ce dernier n'instanciera pas du tout le modèle car aucun type correspondant ne peut être trouvé.

De manière générale, si votre classe / fonction de modèle est conçue pour gérer la classe de modèle en tant que paramètre de modèle, il est préférable de le préciser.

colin
la source
1

Je l'utilise pour les types versionnés.

Si vous avez un type versionné via un modèle tel que MyType<version>, vous pouvez écrire une fonction dans laquelle vous pouvez capturer le numéro de 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)
    {
    ...
    }
}

Vous pouvez donc faire différentes choses en fonction de la version du type transmis au lieu d'avoir une surcharge pour chaque type. Vous pouvez également avoir des fonctions de conversion qui acceptent MyType<Version>et retournent MyType<Version+1>, de manière générique, et même les récurrent pour avoir une ToNewest()fonction qui renvoie la dernière version d'un type de n'importe quelle version plus ancienne (très utile pour les journaux qui auraient pu être stockés il y a quelque temps) mais doivent être traitées avec l'outil le plus récent du jour).

cd127
la source