Interfacce implicite vs esplicite


9

Penso di comprendere le attuali limitazioni del polimorfismo in fase di compilazione e del polimorfismo in fase di esecuzione. Ma quali sono le differenze concettuali tra interfacce esplicite (polimorfismo run-time. Cioè funzioni virtuali e puntatori / riferimenti) e interfacce implicite (polimorfismo tempo di compilazione. Cioè modelli) .

I miei pensieri sono che due oggetti che offrono la stessa interfaccia esplicita devono essere lo stesso tipo di oggetto (o hanno un antenato comune), mentre due oggetti che offrono la stessa interfaccia implicita non devono necessariamente essere lo stesso tipo di oggetto e, escludendo l'implicito l'interfaccia che entrambi offrono, può avere funzionalità abbastanza diverse.

Qualche idea su questo?

E se due oggetti offrono la stessa interfaccia implicita, quali sono i motivi (oltre al vantaggio tecnico di non aver bisogno di un dispacciamento dinamico con una tabella di ricerca delle funzioni virtuali, ecc.) Per non avere questi oggetti ereditati da un oggetto base che dichiara tale interfaccia, quindi rendendolo un'interfaccia esplicita ? Un altro modo di dirlo: puoi darmi un caso in cui due oggetti che offrono la stessa interfaccia implicita (e quindi possono essere usati come tipi per la classe modello di esempio) non dovrebbero ereditare da una classe base che rende esplicita tale interfaccia?

Alcuni post correlati:


Ecco un esempio per rendere più concreta questa domanda:

Interfaccia implicita:

class Class1
{
public:
  void interfaceFunc();
  void otherFunc1();
};

class Class2
{
public:
  void interfaceFunc();
  void otherFunc2();
};

template <typename T>
class UseClass
{
public:
  void run(T & obj)
  {
    obj.interfaceFunc();
  }
};

Interfaccia esplicita:

class InterfaceClass
{
public:
  virtual void interfaceFunc() = 0;
};

class Class1 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc1();
};

class Class2 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc2();
};

class UseClass
{
public:
  void run(InterfaceClass & obj)
  {
    obj.interfaceFunc();
  }
};

Un esempio ancora più approfondito e concreto:

Alcuni problemi di C ++ possono essere risolti con:

  1. una classe modello il cui tipo di modello fornisce un'interfaccia implicita
  2. una classe non basata su modelli che accetta un puntatore di classe base che fornisce un'interfaccia esplicita

Codice che non cambia:

class CoolClass
{
public:
  virtual void doSomethingCool() = 0;
  virtual void worthless() = 0;
};

class CoolA : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that an A would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

class CoolB : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that a B would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

Caso 1 . Una classe non basata su modelli che accetta un puntatore di classe base che fornisce un'interfaccia esplicita:

class CoolClassUser
{
public:  
  void useCoolClass(CoolClass * coolClass)
  { coolClass.doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Caso 2 . Una classe basata su modelli il cui tipo di modello fornisce un'interfaccia implicita:

template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser<CoolClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Caso 3 . Una classe basata su modelli il cui tipo di modello fornisce un'interfaccia implicita (questa volta, non derivante da CoolClass:

class RandomClass
{
public:
  void doSomethingCool()
  { /* Do cool stuff that a RandomClass would do */ }

  // I don't have to implement worthless()! Na na na na na!
}


template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  RandomClass * c1 = new RandomClass;
  RandomClass * c2 = new RandomClass;

  CoolClassUser<RandomClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Il caso 1 richiede che l'oggetto che viene passato useCoolClass()sia figlio di CoolClass(e attrezzo worthless()). I casi 2 e 3, d'altra parte, prenderanno qualsiasi classe che ha una doSomethingCool()funzione.

Se gli utenti del codice sono sempre stati bene sottoclasse CoolClass, allora il caso 1 ha un senso intuitivo, poiché si CoolClassUseraspetterebbe sempre un'implementazione di a CoolClass. Ma supponiamo che questo codice faccia parte di un framework API, quindi non posso prevedere se gli utenti vorranno sottoclassare CoolClasso eseguire il roll della propria classe che ha una doSomethingCool()funzione.


Forse mi manca qualcosa, ma l'importante differenza non è già stata succinta nel tuo primo paragrafo, ovvero che le interfacce esplicite sono polimorfismo di runtime, mentre le interfacce implicite sono polimorfismo di compilazione?
Robert Harvey

2
Esistono alcuni problemi che possono essere risolti con una Classe o una funzione che porta un puntatore a una Classe astratta (che fornisce un'interfaccia esplicita) o con una Classe o una funzione basata su modelli che utilizza un oggetto che fornisce un'interfaccia implicita. Entrambe le soluzioni funzionano. Quando vorresti utilizzare la prima soluzione? Il secondo?
Chris Morris,

penso che la maggior parte di queste considerazioni cadano in pezzi quando apri un po 'di più i concetti. per esempio, dove inseriresti il ​​polimorfismo statico non ereditario?
Javier

Risposte:


8

Hai già definito il punto importante: uno è il tempo di esecuzione e l'altro è il tempo di compilazione . Le informazioni reali di cui hai bisogno sono le ramificazioni di questa scelta.

compiletime:

  • Pro: le interfacce di compilazione sono molto più granulari di quelle di runtime. Con ciò, ciò che intendo è che puoi usare solo i requisiti di una singola funzione, o un insieme di funzioni, come li chiami. Non devi fare sempre l'intera interfaccia. I requisiti sono solo ed esattamente ciò di cui hai bisogno.
  • Pro: Tecniche come CRTP significano che puoi usare interfacce implicite per implementazioni predefinite di cose come gli operatori. Non potresti mai fare una cosa del genere con l'eredità di runtime.
  • Pro: le interfacce implicite sono molto più facili da comporre e moltiplicare "ereditare" rispetto alle interfacce di runtime e non impongono alcun tipo di restrizioni binarie, ad esempio le classi POD possono utilizzare interfacce implicite. Non c'è bisogno di virtualereditarietà o altri shenanigani con interfacce implicite, un grande vantaggio.
  • Pro: il compilatore può fare molte più ottimizzazioni per le interfacce in fase di compilazione. Inoltre, la sicurezza aggiuntiva del tipo rende il codice più sicuro.
  • Pro: È impossibile eseguire la digitazione di valore per le interfacce di runtime, perché non si conoscono le dimensioni o l'allineamento dell'oggetto finale. Ciò significa che ogni caso che necessita / beneficia della digitazione di valore ottiene grandi benefici dai modelli.
  • Contro: i template sono una puttana da compilare e usare e possono essere portati in modo complicato tra i compilatori
  • Contro: i modelli non possono essere caricati in fase di esecuzione (ovviamente), quindi hanno dei limiti nell'espressione delle strutture di dati dinamici, ad esempio.

Durata:

  • Pro: il tipo finale non deve essere deciso fino al runtime. Ciò significa che l'ereditarietà di runtime può esprimere alcune strutture di dati molto più facilmente, se i modelli possono farlo. Inoltre, è possibile esportare tipi polimorfici di runtime attraverso i limiti C, ad esempio COM.
  • Pro: è molto più semplice specificare e implementare l'ereditarietà di runtime e non otterrai alcun comportamento specifico del compilatore.
  • Contro: l'ereditarietà in fase di esecuzione può essere più lenta dell'ereditarietà in fase di compilazione.
  • Contro: l'ereditarietà di runtime perde le informazioni sul tipo.
  • Contro: l'eredità di runtime è molto meno flessibile.
  • Contro: l'ereditarietà multipla è una cagna.

Dato l'elenco relativo, se non è necessario uno specifico vantaggio dell'ereditarietà di runtime, non utilizzarlo. È più lento, meno flessibile e meno sicuro dei modelli.

Modifica: Vale la pena notare che in C ++ in particolare ci sono usi per ereditarietà diversi dal polimorfismo di runtime. Ad esempio, è possibile ereditare i typedef o utilizzarlo per la codifica dei tipi o utilizzare il CRTP. Alla fine, però, queste tecniche (e altre) rientrano davvero in "Compile-time", anche se sono implementate usando class X : public Y.


Per quanto riguarda il tuo primo professionista per compiletime, questo è legato a una delle mie domande principali. Vorresti mai chiarire che vuoi lavorare solo con un'interfaccia esplicita. Vale a dire. "Non mi interessa se hai tutte le funzioni di cui ho bisogno, se non erediti dalla Classe Z, allora non voglio avere niente a che fare con te". Inoltre, l'ereditarietà di runtime non perde le informazioni sul tipo quando si utilizzano puntatori / riferimenti, giusto?
Chris Morris,

@ChrisMorris: No. Se funziona, allora funziona, il che è tutto ciò di cui dovresti preoccuparti. Perché far scrivere a qualcuno lo stesso codice altrove?
jmoreno,

1
@ChrisMorris: No, non lo farei. Se ho solo bisogno di X, allora è uno dei principi fondamentali di base dell'incapsulamento che dovrei sempre chiedere e preoccuparmi di X. Inoltre, perde informazioni sul tipo. Ad esempio, non è possibile assegnare in pila un oggetto di questo tipo. Non è possibile creare un'istanza di un modello con il suo tipo vero. Non è possibile chiamare funzioni membro con modelli su di esse.
DeadMG

Che dire di una situazione in cui hai una classe Q che usa una classe. Q accetta un parametro template, quindi qualsiasi classe che fornisce l'interfaccia implicita lo farà, o almeno così pensiamo. Si scopre che anche la classe Q si aspetta che la sua classe interna (chiamala H) usi l'interfaccia di Q. Ad esempio, quando l'oggetto H viene distrutto, dovrebbe chiamare alcune funzioni di Q's. Questo non può essere specificato in un'interfaccia implicita. Pertanto, i modelli falliscono. Più chiaramente, un insieme di classi strettamente accoppiate che richiede più di interfacce implicite l'una dall'altra sembra compromettere l'uso dei modelli.
Chris Morris,

Con compiletime: Brutto da debug, necessità di inserire le definizioni nell'intestazione
JFFIGK
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.