Question

Sur mon lieu de travail, nous avons tendance à utiliser iostream , chaîne , vecteur , map , et l'intrus algorithme ou deux. Nous n'avons pas encore trouvé beaucoup de situations où les techniques de gabarit étaient la meilleure solution à un problème.

Ce que je recherche ici, ce sont des idées et éventuellement un exemple de code qui montre comment vous avez utilisé une technique de gabarit pour créer une nouvelle solution à un problème rencontré dans la vie réelle.

En tant que pot-de-vin, attendez-vous à un vote positif pour votre réponse.

Était-ce utile?

La solution

J'ai utilisé beaucoup de code de gabarit, principalement dans Boost et dans la STL, mais j'ai rarement eu besoin d'en en écrire .

L'une des exceptions, il y a quelques années, concernait un programme manipulant des fichiers EXE au format Windows PE. La société souhaitait ajouter une prise en charge 64 bits, mais la classe ExeFile que j'avais écrite pour gérer les fichiers ne fonctionnait qu'avec des fichiers 32 bits. Le code requis pour manipuler la version 64 bits était essentiellement identique, mais il fallait utiliser un type d'adresse différent (64 bits au lieu de 32 bits), ce qui entraînait également la différence de deux autres structures de données.

Sur la base de l'utilisation par la STL d'un seul modèle pour prendre en charge std::string et std::wstring, j'ai décidé d'essayer de créer #ifdef WIN64 un modèle avec les différentes structures de données et le type d'adresse en tant que paramètres. Il y avait deux endroits où je devais encore utiliser <=> des lignes (exigences de traitement légèrement différentes), mais ce n'était pas vraiment difficile à faire. Ce programme offre désormais une prise en charge complète des formats 32 et 64 bits, et l’utilisation du modèle signifie que toutes les modifications apportées depuis s’appliquent automatiquement aux deux versions.

Autres conseils

Informations générales sur les modèles:

Les modèles sont utiles chaque fois que vous devez utiliser le même code, mais fonctionnant sur différents types de données, les types étant connus au moment de la compilation. Et aussi lorsque vous avez n'importe quel type d'objet conteneur.

Un usage très courant concerne pratiquement tous les types de structure de données. Par exemple: listes liées individuellement, listes associées, arborescences, tentatives, tables de hachage, ...

Une autre utilisation très courante concerne le tri des algorithmes.

L'un des principaux avantages de l'utilisation de modèles est que vous pouvez supprimer la duplication de code. La duplication de code est l’un des principaux problèmes à éviter lors de la programmation.

Vous pouvez implémenter une fonction Max en tant que macro ou modèle, mais l'implémentation du modèle serait sécurisée et donc meilleure.

Et maintenant, passons aux choses intéressantes:

Voir aussi la métaprogrammation du modèle , qui permet de pré-évaluer le code au moment de la compilation. plutôt qu'au moment de l'exécution. La métaprogrammation des modèles ne comporte que des variables immuables et ses variables ne peuvent donc pas changer. En raison de ce modèle, la métaprogrammation peut être considérée comme un type de programmation fonctionnelle.

Découvrez cet exemple de métaprogrammation de modèles de Wikipedia. Il montre comment les modèles peuvent être utilisés pour exécuter du code au moment de la compilation . Par conséquent, au moment de l'exécution, vous avez une constante pré-calculée.

template <int N>
struct Factorial 
{
    enum { value = N * Factorial<N - 1>::value };
};

template <>
struct Factorial<0> 
{
    enum { value = 1 };
};

// Factorial<4>::value == 24
// Factorial<0>::value == 1
void foo()
{
    int x = Factorial<4>::value; // == 24
    int y = Factorial<0>::value; // == 1
}

J'utilise des modèles pour créer mon propre code, notamment, à la mise en oeuvre de classes de règles, comme décrit par Andrei Alexandrescu dans Modern C ++ Design. Je travaille actuellement sur un projet qui inclut un ensemble de classes qui interagissent avec le moniteur Tuxedo TP d’Oracle de BEA \ h \ h \ h.

Une des installations fournies par Tuxedo est les files d'attente persistantes transactionnelles. J'ai donc une classe TpQueue qui interagit avec la file d'attente:

class TpQueue {
public:
   void enqueue(...)
   void dequeue(...)
   ...
}

Cependant, comme la file d'attente est transactionnelle, je dois décider du comportement de transaction que je souhaite; cela pourrait être fait séparément en dehors de la classe TpQueue mais je pense que c'est plus explicite et moins sujet aux erreurs si chaque instance de TpQueue a sa propre politique sur les transactions. J'ai donc un ensemble de classes TransactionPolicy telles que:

class OwnTransaction {
public:
   begin(...)  // Suspend any open transaction and start a new one
   commit(..)  // Commit my transaction and resume any suspended one
   abort(...)
}

class SharedTransaction {
public:
   begin(...)  // Join the currently active transaction or start a new one if there isn't one
   ...
}

Et la classe TpQueue est ré-écrite comme

template <typename TXNPOLICY = SharedTransaction>
class TpQueue : public TXNPOLICY {
   ...
}

Donc, à l'intérieur de TpQueue, je peux appeler begin (), abort (), commit () selon les besoins, mais je peux modifier le comportement en fonction de la manière dont je déclare l'instance:

TpQueue<SharedTransaction> queue1 ;
TpQueue<OwnTransaction> queue2 ;

J’ai utilisé des modèles (à l’aide de Boost.Fusion) pour obtenir des entiers sûrs pour la bibliothèque de hypergraphes que je développais. J'ai un (hyper) ID de bord et un ID de sommet, qui sont tous deux des entiers. Avec les modèles, les identifiants de vertex et d'hyperge devinrent différents types et générer une erreur lors de la compilation en utilisant l'un quand l'autre était attendu. M'a sauvé beaucoup de maux de tête que j'aurais autrement avec le débogage au moment de l'exécution.

Voici un exemple tiré d'un projet réel. J'ai des fonctions de lecture comme ceci:

bool getValue(wxString key, wxString& value);
bool getValue(wxString key, int& value);
bool getValue(wxString key, double& value);
bool getValue(wxString key, bool& value);
bool getValue(wxString key, StorageGranularity& value);
bool getValue(wxString key, std::vector<wxString>& value);

Et ensuite une variante avec la valeur 'default'. Il retourne la valeur de la clé si elle existe, ou la valeur par défaut si ce n'est pas le cas. Ce modèle m'a évité d'avoir à créer moi-même 6 nouvelles fonctions.

template <typename T>
T get(wxString key, const T& defaultValue)
{
    T temp;
    if (getValue(key, temp))
        return temp;
    else
        return defaultValue;
}

Les modèles que je consomme régulièrement sont une multitude de classes de conteneur, des pointeurs intelligents, scopeguards , quelques algorithmes STL.

Scénarios dans lesquels j'ai écrit des modèles:

  • conteneurs personnalisés
  • gestion de la mémoire, implémentation du type sécurité et invocation CTor / DT ou au-dessus des allocateurs void *
  • implémentation commune pour les surcharges avec différents types, par exemple

    bool ContainsNan (float *, int) bool ContainsNan (double *, int)

qui appellent tous deux une fonction d'assistance (locale, cachée)

template <typename T>
bool ContainsNanT<T>(T * values, int len) { ... actual code goes here } ;

Algorithmes spécifiques indépendants du type, pour autant que le type ait certaines propriétés, par ex. la sérialisation binaire.

template <typename T>
void BinStream::Serialize(T & value) { ... }

// to make a type serializable, you need to implement
void SerializeElement(BinStream & strean, Foo & element);
void DeserializeElement(BinStream & stream, Foo & element)

Contrairement aux fonctions virtuelles, les modèles permettent davantage d'optimisations.

En règle générale, les modèles permettent d'implémenter un concept ou un algorithme pour une multitude de types et ont déjà résolu les différences au moment de la compilation.

Nous utilisons COM et acceptons un pointeur sur un objet pouvant directement implémenter une autre interface ou via [IServiceProvider] ( http://msdn.microsoft.com/en-us/library/cc678965 (VS.85) .aspx) Cela m'a incité à créer cette fonction semblable à une distribution.

// Get interface either via QueryInterface of via QueryService
template <class IFace>
CComPtr<IFace> GetIFace(IUnknown* unk)
{
    CComQIPtr<IFace> ret = unk; // Try QueryInterface
    if (ret == NULL) { // Fallback to QueryService
        if(CComQIPtr<IServiceProvider> ser = unk)
            ser->QueryService(__uuidof(IFace), __uuidof(IFace), (void**)&ret);
    }
    return ret;
}

J'utilise des modèles pour spécifier les types d'objet fonction. J'écris souvent du code qui prend un objet de fonction comme argument - une fonction à intégrer, une fonction à optimiser, etc. - et je trouve les modèles plus pratiques que l'héritage. Ainsi, mon code recevant un objet fonction, tel qu'un intégrateur ou un optimiseur, dispose d'un paramètre de modèle pour spécifier le type d'objet fonction sur lequel il opère.

Outre les raisons évidentes (telles que la prévention de la duplication de code en utilisant différents types de données), il existe un modèle vraiment génial appelé conception basée sur des règles. J'ai posé une question sur les stratégies vs stratégies .

Maintenant, en quoi cette fonctionnalité est-elle intéressante? Considérez que vous écrivez une interface pour les autres à utiliser. Vous savez que votre interface sera utilisée car il s'agit d'un module dans son propre domaine. Mais vous ne savez pas encore comment les gens vont l'utiliser. La conception basée sur des règles renforce votre code pour une réutilisation future. cela vous rend indépendant des types de données sur lesquels une implémentation particulière repose. Le code est juste & ";" Insufflé dans & "; : -)

Les traits sont en soi une excellente idée. Ils peuvent associer un comportement, des données et des typedata particuliers à un modèle. Les traits permettent un paramétrage complet de chacun de ces trois champs. Et le meilleur, c’est un très bon moyen de rendre le code réutilisable.

J'ai déjà vu le code suivant:

void doSomethingGeneric1(SomeClass * c, SomeClass & d)
{
   // three lines of code
   callFunctionGeneric1(c) ;
   // three lines of code
}

répété dix fois:

void doSomethingGeneric2(SomeClass * c, SomeClass & d)
void doSomethingGeneric3(SomeClass * c, SomeClass & d)
void doSomethingGeneric4(SomeClass * c, SomeClass & d)
// Etc

Chaque fonction ayant les mêmes 6 lignes de code copiées / collées et appelant à chaque fois une autre fonction callFunctionGenericX portant le même suffixe numérique.

Il n’existait aucun moyen de remanier le tout. J'ai donc gardé le refactoring local.

J'ai changé le code de cette façon (de mémoire):

template<typename T>
void doSomethingGenericAnything(SomeClass * c, SomeClass & d, T t)
{
   // three lines of code
   t(c) ;
   // three lines of code
}

Et modifié le code existant avec:

void doSomethingGeneric1(SomeClass * c, SomeClass & d)
{
   doSomethingGenericAnything(c, d, callFunctionGeneric1) ;
}

void doSomethingGeneric2(SomeClass * c, SomeClass & d)
{
   doSomethingGenericAnything(c, d, callFunctionGeneric2) ;
}

Etc.

Ceci est un peu abusif en matière de template, mais au final, je suppose que c'est mieux que de jouer avec des pointeurs de fonction dactylographiés ou d'utiliser des macros.

J'ai personnellement utilisé le modèle de modèle curieusement récurrent pour appliquer une forme de conception descendante et une implémentation ascendante. Un exemple serait une spécification pour un gestionnaire générique où certaines exigences à la fois sur la forme et sur l'interface sont appliquées aux types dérivés au moment de la compilation. Cela ressemble à quelque chose comme ça:

template <class Derived>
struct handler_base : Derived {
  void pre_call() {
    // do any universal pre_call handling here
    static_cast<Derived *>(this)->pre_call();
  };

  void post_call(typename Derived::result_type & result) {
    static_cast<Derived *>(this)->post_call(result);
    // do any universal post_call handling here
  };

  typename Derived::result_type
  operator() (typename Derived::arg_pack const & args) {
    pre_call();
    typename Derived::result_type temp = static_cast<Derived *>(this)->eval(args);
    post_call(temp);
    return temp;
  };

};

Vous pouvez utiliser quelque chose comme cela pour vous assurer que vos gestionnaires dérivent de ce modèle et appliquent la conception descendante, puis permettent une personnalisation ascendante:

struct my_handler : handler_base<my_handler> {
  typedef int result_type; // required to compile
  typedef tuple<int, int> arg_pack; // required to compile
  void pre_call(); // required to compile
  void post_call(int &); // required to compile
  int eval(arg_pack const &); // required to compile
};

Ceci vous permet alors d’avoir des fonctions polymorphes génériques qui traitent uniquement de handler_base < > types dérivés:

template <class T, class Arg0, class Arg1>
typename T::result_type
invoke(handler_base<T> & handler, Arg0 const & arg0, Arg1 const & arg1) {
  return handler(make_tuple(arg0, arg1));
};

Il a déjà été mentionné que vous pouvez utiliser des modèles en tant que classes de règles pour faire quelque chose . J'utilise beaucoup cela.

Je les utilise également, à l'aide de cartes de propriétés ( voir le site boost pour plus d'informations à ce sujet ), afin d'accéder aux données de manière générique. Cela donne la possibilité de changer la façon dont vous stockez les données, sans jamais avoir à changer la façon dont vous les récupérez.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top