Domanda

Se voglio rendere adattabile una classe e rendere possibile selezionare diversi algoritmi dall'esterno, qual è la migliore implementazione in C ++?

Vedo principalmente due possibilità:

  • Usa una classe base astratta e passa l'oggetto concreto in
  • Utilizza un modello

Ecco un piccolo esempio, implementato nelle varie versioni:

Versione 1: classe base astratta

class Brake {
public: virtual void stopCar() = 0;  
};

class BrakeWithABS : public Brake {
public: void stopCar() { ... }
};

class Car {
  Brake* _brake;
public:
  Car(Brake* brake) : _brake(brake) { brake->stopCar(); }
};

Versione 2a: modello

template<class Brake>
class Car {
  Brake brake;
public:
  Car(){ brake.stopCar(); }
};

Versione 2b: modello ed eredità privata

template<class Brake>
class Car : private Brake {
  using Brake::stopCar;
public:
  Car(){ stopCar(); }
};

Proveniente da Java, sono naturalmente propenso a utilizzare sempre la versione 1, ma le versioni dei modelli sembrano essere preferite spesso, ad es. nel codice STL? Se questo è vero, è solo a causa dell'efficienza della memoria, ecc. (Nessuna eredità, nessuna chiamata di funzione virtuale)?

Mi rendo conto che non c'è una grande differenza tra la versione 2a e 2b, vedi Domande frequenti su C ++ .

Puoi commentare queste possibilità?

È stato utile?

Soluzione

Questo dipende dai tuoi obiettivi. Puoi utilizzare la versione 1 se

  • Intende sostituire i freni di un'auto (in fase di esecuzione)
  • Intendi trasferire Car alle funzioni non template

Preferirei di solito la versione 1 usando il polimorfismo di runtime, perché è ancora flessibile e ti permette di avere l'Automobile ancora dello stesso tipo: Car<Opel> è un altro tipo di Car<Nissan>. Se i tuoi obiettivi sono grandi prestazioni durante l'utilizzo frequente dei freni, ti consiglio di utilizzare l'approccio basato su modelli. A proposito, questo si chiama design basato su criteri. Fornisci una politica sui freni . Esempio perché hai detto di aver programmato in Java, probabilmente non hai ancora esperienza con C ++. Un modo per farlo:

template<typename Accelerator, typename Brakes>
class Car {
    Accelerator accelerator;
    Brakes brakes;

public:
    void brake() {
        brakes.brake();
    }
}

Se hai molte politiche puoi raggrupparle nella loro struttura e passarle, ad esempio come SpeedConfiguration raccolta Accelerator, Brakes e altre ancora. Nei miei progetti cerco di mantenere una buona parte del codice libero da template, permettendogli di essere compilato una volta nei loro file oggetto, senza bisogno del loro codice nelle intestazioni, ma ancora permettendo il polimorfismo (tramite funzioni virtuali). Ad esempio, potresti voler mantenere dati e funzioni comuni che il codice non modello probabilmente chiamerà in molte occasioni in una classe base:

class VehicleBase {
protected:
    std::string model;
    std::string manufacturer;
    // ...

public:
    ~VehicleBase() { }
    virtual bool checkHealth() = 0;
};


template<typename Accelerator, typename Breaks>
class Car : public VehicleBase {
    Accelerator accelerator;
    Breaks breaks;
    // ...

    virtual bool checkHealth() { ... }
};

Per inciso, questo è anche l'approccio utilizzato dagli stream C ++: std::ios_base contiene flag e cose che non dipendono dal tipo di carattere o tratti come openmode, formattazione di flag e cose, mentre std::basic_ios è quindi un modello di classe che lo eredita. Ciò riduce anche il gonfiore del codice condividendo il codice comune a tutte le istanze di un modello di classe.

Eredità privata?

L'eredità privata dovrebbe essere evitata in generale. È molto raramente utile e il contenimento è un'idea migliore nella maggior parte dei casi. Caso comune in cui è vero il contrario quando la dimensione è davvero cruciale (classe di stringa basata su criteri, ad esempio): l'ottimizzazione della classe base vuota può essere applicata quando si deriva da una classe politica vuota (contenente solo funzioni).

Leggi Usi e abusi dell'ereditarietà di Herb Sutter.

Altri suggerimenti

La regola empirica è:

1) Se la scelta del tipo di calcestruzzo viene effettuata in fase di compilazione, preferire un modello. Sarà più sicuro (errori di compilazione rispetto a errori di runtime) e probabilmente ottimizzato meglio. 2) Se la scelta viene effettuata in fase di esecuzione (ad es. A seguito dell'azione di un utente) non esiste davvero alcuna scelta: utilizzare l'ereditarietà e le funzioni virtuali.

Altre opzioni:

  1. Utilizza il Pattern visitatore (lascia che il codice esterno funzioni sulla tua classe).
  2. Esternalizza alcune parti della tua classe, ad esempio tramite iteratori, su cui può funzionare un codice generico basato su iteratore. Funziona meglio se il tuo oggetto è un contenitore di altri oggetti.
  3. Vedi anche il Pattern di strategia (ci sono esempi c ++ all'interno)

I modelli sono un modo per consentire a una classe di usare una variabile di cui non ti interessa davvero il tipo. L'ereditarietà è un modo per definire ciò che una classe si basa sui suoi attributi. È il & Quot; is-a & Quot; contro " presenta una domanda " .

Alla maggior parte delle tue domande è già stata data una risposta, ma volevo approfondire questo aspetto:

  

Proveniente da Java, sono naturale   propenso a usare sempre la versione 1, ma   le versioni dei modelli sembrano essere   preferito spesso, ad es. nel codice STL? Se   è vero, è solo a causa di   efficienza di memoria ecc. (nessuna eredità,   nessuna chiamata di funzione virtuale)?

Fa parte di esso. Ma un altro fattore è la sicurezza del tipo aggiunto. Quando trattate un BrakeWithABS come un Brake, perdete le informazioni sul tipo. Non sai più che l'oggetto è in realtà un stopCar(). Se si tratta di un parametro modello, è disponibile il tipo esatto, che in alcuni casi potrebbe consentire al compilatore di eseguire un migliore controllo dei caratteri. Oppure può essere utile per garantire che venga chiamato il sovraccarico corretto di una funzione. (se <=> passa l'oggetto Brake a una seconda funzione, che potrebbe avere un sovraccarico separato per <=>, che non verrà chiamato se avessi usato l'ereditarietà e il tuo <=> fosse stato impostato su <= >.

Un altro fattore è che consente una maggiore flessibilità. Perché tutte le implementazioni di Brake devono ereditare dalla stessa classe base? La classe base ha davvero qualcosa da portare al tavolo? Se scrivo una classe che espone le funzioni membro previste, non è abbastanza buono per agire come un freno? Spesso, l'uso esplicito di interfacce o classi base astratte vincola il codice più del necessario.

(Nota, non sto dicendo che i modelli dovrebbero essere sempre la soluzione preferita. Ci sono altre preoccupazioni che potrebbero influire su questo, che vanno dalla velocità di compilazione a " ciò che i programmatori del mio team hanno familiarità con " ; o solo " cosa preferisco " ;. E a volte, hai bisogno di polimorfismo di runtime, nel qual caso la soluzione template semplicemente non è possibile)

questa risposta è più o meno corretta. Quando vuoi qualcosa di parametrizzato in fase di compilazione, dovresti preferire i template. Quando vuoi qualcosa di parametrizzato in fase di esecuzione, dovresti preferire che le funzioni virtuali vengano ignorate.

Tuttavia , l'uso dei modelli non ti impedisce di fare entrambi (rendendo la versione del modello più flessibile):

struct Brake {
    virtual void stopCar() = 0;
};

struct BrakeChooser {
    BrakeChooser(Brake *brake) : brake(brake) {}
    void stopCar() { brake->stopCar(); }

    Brake *brake;
};

template<class Brake>
struct Car
{
    Car(Brake brake = Brake()) : brake(brake) {}
    void slamTheBrakePedal() { brake.stopCar(); }

    Brake brake;
};


// instantiation
Car<BrakeChooser> car(BrakeChooser(new AntiLockBrakes()));

Detto questo, probabilmente NON userò modelli per questo ... Ma è davvero solo un gusto personale.

La classe base astratta ha un sovraccarico di chiamate virtuali ma ha il vantaggio che tutte le classi derivate sono in realtà classi base. Non è così quando si usano i template & # 8211; & Auto lt; gt freno &; e Car < BrakeWithABS > non sono correlati tra loro e dovrai Dynamic_cast e verificare la presenza di null o disporre di modelli per tutto il codice relativo a Car.

Usa l'interfaccia se supponi di supportare contemporaneamente diverse classi Break e la sua gerarchia.

Car( new Brake() )
Car( new BrakeABC() )
Car( new CoolBrake() )

E non conosci queste informazioni al momento della compilazione.

Se sai quale Break stai per usare 2b è la scelta giusta per te per specificare diverse classi di auto. Il freno in questo caso sarà la tua auto & Quot; Strategia & Quot; e puoi impostarne uno predefinito.

Non userei 2a. Invece puoi aggiungere metodi statici a Break e chiamarli senza istanza.

Personalmente preferirei sempre usare le interfacce sui template per diversi motivi:

  1. Modelli & compilazione di amp; a volte gli errori di collegamento sono criptici
  2. È difficile eseguire il debug di un codice basato su modelli (almeno nell'IDE di Visual Studio)
  3. I modelli possono ingrandire i tuoi binari.
  4. I modelli richiedono di inserire tutto il suo codice nel file di intestazione, il che rende la classe del modello un po 'più difficile da capire.
  5. I modelli sono difficili da gestire per i programmatori alle prime armi.

Uso i modelli solo quando le tabelle virtuali creano un tipo di overhead.

Naturalmente, questa è solo la mia opinione personale.

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