Domanda

Nel mio posto di lavoro, tendiamo a usare iostream, corda, vettore, carta geografica, e lo strano algoritmo o due.In realtà non abbiamo trovato molte situazioni in cui le tecniche dei modelli fossero la soluzione migliore a un problema.

Ciò che cerco qui sono idee e, facoltativamente, codice di esempio che mostri come hai utilizzato una tecnica di modello per creare una nuova soluzione a un problema riscontrato nella vita reale.

Come tangente, aspettati un voto positivo per la tua risposta.

È stato utile?

Soluzione

Ho usato molto codice modello, principalmente in Boost e STL, ma raramente avevo bisogno di scrivere qualsiasi.

Una delle eccezioni, alcuni anni fa, era in un programma che manipolava i file EXE in formato Windows PE. La società voleva aggiungere il supporto a 64 bit, ma la classe ExeFile che avevo scritto per gestire i file funzionava solo con quelli a 32 bit. Il codice richiesto per manipolare la versione a 64 bit era essenzialmente identico, ma doveva utilizzare un diverso tipo di indirizzo (64 bit anziché 32 bit), il che causava differenze anche con altre due strutture di dati.

Basandomi sull'uso da parte della STL di un singolo modello per supportare sia std::string sia std::wstring, ho deciso di provare a creare #ifdef WIN64 un modello, con le diverse strutture di dati e il tipo di indirizzo come parametri. C'erano due posti in cui dovevo ancora usare <=> linee (requisiti di elaborazione leggermente diversi), ma non era davvero difficile da fare. Ora abbiamo pieno supporto a 32 e 64 bit in quel programma e l'uso del modello significa che ogni modifica che abbiamo fatto da allora si applica automaticamente ad entrambe le versioni.

Altri suggerimenti

Informazioni generali sui modelli:

I modelli sono utili ogni volta che è necessario utilizzare lo stesso codice ma operando su tipi di dati diversi, in cui i tipi sono noti al momento della compilazione. E anche quando hai qualsiasi tipo di oggetto container.

Un utilizzo molto comune è per quasi ogni tipo di struttura di dati. Ad esempio: elenchi collegati singolarmente, elenchi doppiamente collegati, alberi, tentativi, hashtabili, ...

Un altro uso molto comune è per gli algoritmi di ordinamento.

Uno dei principali vantaggi dell'utilizzo dei modelli è che è possibile rimuovere la duplicazione del codice. La duplicazione del codice è una delle cose più importanti da evitare durante la programmazione.

Potresti implementare una funzione Max sia come macro che come modello, ma l'implementazione del modello sarebbe di tipo sicuro e quindi migliore.

E ora sulle cose interessanti:

Vedi anche metaprogrammazione del modello , che è un modo per pre-valutare il codice al momento della compilazione piuttosto che in fase di esecuzione. La metaprogrammazione dei modelli ha solo variabili immutabili e quindi le sue variabili non possono cambiare. A causa di questo modello, la metaprogrammazione può essere vista come un tipo di programmazione funzionale.

Dai un'occhiata a questo esempio di metaprogrammazione dei modelli da Wikipedia. Mostra come i modelli possono essere utilizzati per eseguire il codice in fase di compilazione . Pertanto in fase di esecuzione hai una costante pre-calcolata.

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
}

Un posto in cui utilizzo i modelli per creare il mio codice è quello di implementare le classi politiche come descritto da Andrei Alexandrescu in Modern C ++ Design. Al momento sto lavorando a un progetto che include una serie di classi che interagiscono con il monitor Tuxedo TP Oracle di BEA \ h \ h \ h.

Una funzione fornita da Tuxedo sono le code permanenti transazionali, quindi ho una classe TpQueue che interagisce con la coda:

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

Tuttavia, poiché la coda è transazionale, devo decidere quale comportamento di transazione voglio; questo potrebbe essere fatto separatamente al di fuori della classe TpQueue ma penso che sia più esplicito e meno soggetto a errori se ogni istanza di TpQueue ha la sua politica sulle transazioni. Quindi ho una serie di classi TransactionPolicy come:

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

E la classe TpQueue viene riscritta come

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

Quindi all'interno di TpQueue posso chiamare begin (), abort (), commit () secondo necessità, ma posso cambiare il comportamento in base al modo in cui dichiaro l'istanza:

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

Ho usato i template (con l'aiuto di Boost.Fusion) per ottenere numeri interi sicuri per una libreria di hypergraph che stavo sviluppando. Ho un ID (hyper) edge e un ID vertice entrambi i quali sono numeri interi. Con i modelli, gli ID vertice e hyperedge sono diventati tipi diversi e l'utilizzo di uno quando era previsto l'altro ha generato un errore di compilazione. Mi ha risparmiato un sacco di mal di testa che altrimenti avrei con il debug di runtime.

Ecco un esempio di un vero progetto. Ho funzioni getter come questa:

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

E poi una variante con il valore 'predefinito'. Restituisce il valore per chiave se esiste o valore predefinito in caso contrario. Template mi ha salvato dal dover creare 6 nuove funzioni da solo.

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

I modelli che consumo regolarmente sono una moltitudine di classi container, aumentano i puntatori intelligenti, scopeguards , alcuni algoritmi STL.

Scenari in cui ho scritto modelli:

  • contenitori personalizzati
  • gestione della memoria, implementazione della sicurezza dei tipi e invocazione di CTor / DTor in cima a * allocatori
  • implementazione comune per sovraccarichi con tipi diversi, ad esempio

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

che entrambi chiamano semplicemente una funzione di supporto (locale, nascosta)

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

Algoritmi specifici indipendenti dal tipo, purché il tipo abbia determinate proprietà, ad es. serializzazione binaria.

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)

A differenza delle funzioni virtuali, i modelli consentono di eseguire ulteriori ottimizzazioni.


In generale, i modelli consentono di implementare un concetto o un algoritmo per una moltitudine di tipi e hanno le differenze risolte già al momento della compilazione.

Utilizziamo COM e accettiamo un puntatore a un oggetto che può implementare un'altra interfaccia direttamente o tramite [IServiceProvider] ( http://msdn.microsoft.com/en-us/library/cc678965 (VS.85) .aspx) questo mi ha spinto a creare questa funzione simile a un cast di supporto.

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

Uso i modelli per specificare i tipi di oggetti funzione. Scrivo spesso un codice che accetta un oggetto funzione come argomento - una funzione da integrare, una funzione da ottimizzare, ecc. - e trovo i modelli più convenienti dell'eredità. Quindi il mio codice che riceve un oggetto funzione - come un integratore o un ottimizzatore - ha un parametro template per specificare il tipo di oggetto funzione su cui opera.

I motivi ovvi (come prevenire la duplicazione del codice operando su diversi tipi di dati) a parte, c'è questo modello davvero interessante che si chiama design basato su criteri. Ho posto una domanda sulle politiche contro strategie .

Ora, cosa c'è di così elegante in questa funzione. Considera di scrivere un'interfaccia che gli altri possano usare. Sai che verrà utilizzata la tua interfaccia, perché è un modulo nel proprio dominio. Ma non sai ancora come le persone lo useranno. La progettazione basata su criteri rafforza il codice per il futuro riutilizzo; ti rende indipendente dai tipi di dati su cui si basa una particolare implementazione. Il codice è solo & Quot; inserito in & Quot ;. : -)

I tratti sono di per sé un'idea meravigliosa. Possono associare comportamenti, dati e typedata particolari a un modello. I tratti consentono la completa parametrizzazione di tutti e tre questi campi. Inoltre, è un ottimo modo per rendere riutilizzabile il codice.

Una volta ho visto il seguente codice:

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

ripetuto dieci volte:

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

Ogni funzione ha le stesse 6 righe di copia / incolla del codice e ogni volta chiama un'altra funzione callFunctionGenericX con lo stesso suffisso numerico.

Non c'era modo di refactificare il tutto. Quindi ho mantenuto il refactoring locale.

Ho modificato il codice in questo modo (dalla memoria):

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

E modificato il codice esistente con:

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

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

Etc.

Questo è un po 'highjacking alla cosa modello, ma alla fine, immagino sia meglio che giocare con i puntatori a funzione tipizzata o usare le macro.

Personalmente ho utilizzato il modello di modello curiosamente ricorrente come mezzo per imporre una qualche forma di progettazione top-down e implementazione bottom-up.Un esempio potrebbe essere una specifica per un gestore generico in cui determinati requisiti sia sul modulo che sull'interfaccia vengono applicati ai tipi derivati ​​in fase di compilazione.Sembra qualcosa del genere:

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

};

Qualcosa di simile può essere utilizzato quindi per assicurarsi che i tuoi gestori derivino da questo modello e applichino la progettazione top-down e quindi consentano la personalizzazione dal basso verso l'alto:

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

Ciò consente quindi di avere funzioni polimorfiche generiche che gestiscono solo i tipi derivati ​​handler_base<>:

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

È già stato menzionato che è possibile utilizzare i modelli come classi di criteri per fare qualcosa . Lo uso molto.

Li uso anche con l'aiuto di mappe delle proprietà ( vedi il sito boost per ulteriori informazioni su questo ), al fine di accedere ai dati in modo generico. Ciò offre l'opportunità di cambiare il modo in cui archiviate i dati, senza mai dover cambiare il modo in cui li recuperate.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top