web-dev-qa-db-fra.com

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

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 concevoir des classes basées sur des stratégies. Quels autres usages cette technique a-t-elle?

202
Ferruccio

Je pense que vous devez utiliser la syntaxe de modèle pour transmettre 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) {
}

Ici, H est un modèle, mais je voulais que cette fonction traite toutes les spécialisations de H.

NOTE: Je programme c ++ depuis de nombreuses années et j'en ai seulement besoin une fois. Je trouve que c'est une fonctionnalité rarement nécessaire (bien sûr pratique lorsque 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 imaginons un exemple. Supposons que std::vectorn'a pas un typedef value_type

Alors, comment écririez-vous une fonction qui peut créer des variables du bon type pour les éléments de 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;
}

NOTE: nous std::vector a 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'avons 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!

UPDATE: Même cet exemple artificiel, bien qu'illustratif, n'est plus un exemple étonnant, car c ++ 11 introduit auto. Maintenant, la même fonction peut être écrite ainsi:

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.

167
Evan Teran

En fait, le cas d'utilisation des paramètres de modèle de modèle est plutôt évident. Une fois que vous apprenez que C++ stdlib a le trou béant de ne pas définir les opérateurs de sortie de flux pour les types de conteneur standard, vous pouvez é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;
}

Vous constateriez ensuite que le code de vecteur est identique, que forward_list est identique, même pour une multitude de types de cartes, il reste identique. Ces classes de gabarit n'ont rien en commun, à l'exception de la méta-interface/protocole, et l'utilisation du paramètre de gabarit de gabarit permet de saisir les points communs dans chacune d'elles. Avant de commencer à écrire un modèle, il convient toutefois 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. Alors que allocateur est par défaut, nous devons toujours en rendre compte 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)
...

Voilà, cela fonctionnera automatiquement pour tous les conteneurs de séquences actuels et futurs adhérant au protocole standard. Pour ajouter des cartes à la combinaison, il faudrait jeter un coup d'œ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 à 4 arguments. Nous verrions également que std: pair essaie d’être restitué avec l’opérateur 2-arg << pour les types de séquence que nous avons définis précédemment. Nous allons donc fournir une spécialisation uniquement pour std :: pair. 

Btw, avec C + 11 qui autorise les modèles variadiques (et devrait donc permettre les arguments de modèles variadiques), 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;
}

Sortie

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 
139
pfalcon

Voici un exemple simple tiré de 'Conception moderne en C++ - Programmation générique et modèles de conception appliqués' par Andrei Alexandrescu:

Il utilise des classes avec des paramètres de modèle pour 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 Host connaît déjà, ou peut facilement en déduire, l’argument de modèle de la classe policy. Dans l'exemple ci-dessus, WidgetManager gère toujours les objets de type Widget. Il est donc redondant et potentiellement dangereux que l'utilisateur spécifie à nouveau Widget dans l'instanciation de CreationPolicy. Dans ce cas, le code de bibliothèque peut utiliser des paramètres de modèle de modèle pour spécifier les stratégies.

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 plus lourde et sujette aux erreurs qu'une définition dépourvue d'arguments de modèle de modèle aurait requis:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
60
yoav.aviram

Voici un autre exemple pratique tiré de ma bibliothèque de réseaux de neurones CUDA Convolutional . 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 y a aussi un template de classe enfant:

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

qui implémente les mêmes fonctionnalités mais dans 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 la syntaxe de template de template 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).

18
Mikhail Sirotenko

Supposons que vous utilisiez CRTP pour fournir une "interface" à un ensemble de modèles enfants. et le parent et l'enfant sont tous deux paramétriques dans le ou les 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 que celui 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 du ou des autres paramètres de modèle au dérivé modèle; "l'interface" les reçoit toujours.

Cela vous permet également de construire des "typedef" 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 taper un modèle non spécifié. Cela fonctionne cependant (et C++ 11 a un support natif pour les typedefs de modèles):

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

typedef typename derived_interface_type<int>::type derived_t;

Malheureusement, vous avez besoin d’un type dérivé_interface_type pour chaque instanciation du modèle dérivé, à moins qu’il n’y ait une autre astuce que je n’ai pas encore apprise.

9
Mark McKenna

C'est 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 à:

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();
}
4
Cookie

Dans la solution avec les modèles variadiques fournie par pfalcon, il était 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;
}
4
Kuberan Naganathan

En voici une généralisée à partir de quelque chose que je viens d'utiliser. Je le poste car il s’agit d’un exemple very simple, et il illustre 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;
};
2
imallett

Il améliore la lisibilité de votre code, offre une sécurité de type supplémentaire et évite 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 

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

ou avec le paramètre template template

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, par exemple print_container(3). Dans le premier cas, le template sera instancié par le compilateur qui se plaindra de l'utilisation de c dans la boucle for, le second n'instanciera pas du tout le template 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. 

0
colin