Implizite oder explizite Schnittstellen

9

Ich glaube, ich verstehe die tatsächlichen Einschränkungen des Polymorphismus zur Kompilierungszeit und des Laufzeitpolymorphismus. Aber was sind die konzeptionellen Unterschiede zwischen expliziten Schnittstellen (Laufzeit - Polymorphismus. Dh virtuelle Funktionen und Zeiger / Referenzen) und implizite Schnittstellen (Kompilierung-Polymorphismus. Dh. Vorlagen) .

Ich bin der Meinung, dass zwei Objekte, die dieselbe explizite Schnittstelle bieten, denselben Objekttyp haben müssen (oder einen gemeinsamen Vorfahren haben müssen), während zwei Objekte, die dieselbe implizite Schnittstelle bieten, nicht denselben Objekttyp haben müssen und den impliziten ausschließen Schnittstelle, die beide bieten, können ganz unterschiedliche Funktionen haben.

Irgendwelche Gedanken dazu?

Und wenn zwei Objekte dieselbe implizite Schnittstelle bieten, welche Gründe (neben dem technischen Vorteil, dass kein dynamischer Versand mit einer Nachschlagetabelle für virtuelle Funktionen usw. erforderlich ist) liegen darin, dass diese Objekte nicht von einem Basisobjekt geerbt werden, das diese Schnittstelle deklariert macht es eine explizite Schnittstelle? Eine andere Art, es auszudrücken: Können Sie mir einen Fall nennen, in dem zwei Objekte, die dieselbe implizite Schnittstelle bieten (und daher als Typen für die Beispielvorlagenklasse verwendet werden können), nicht von einer Basisklasse erben sollten, die diese Schnittstelle explizit macht?

Einige verwandte Beiträge:


Hier ist ein Beispiel, um diese Frage konkreter zu machen:

Implizite Schnittstelle:

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

Explizite Schnittstelle:

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

Ein noch ausführlicheres, konkretes Beispiel:

Einige C ++ - Probleme können entweder gelöst werden mit:

  1. Eine Vorlagenklasse, deren Vorlagentyp eine implizite Schnittstelle bereitstellt
  2. Eine Klasse ohne Vorlage, die einen Basisklassenzeiger verwendet, der eine explizite Schnittstelle bereitstellt

Code, der sich nicht ändert:

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

Fall 1 . Eine Klasse ohne Vorlage, die einen Basisklassenzeiger verwendet, der eine explizite Schnittstelle bietet:

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

Fall 2 . Eine Vorlagenklasse, deren Vorlagentyp eine implizite Schnittstelle bietet:

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

Fall 3 . Eine Vorlagenklasse, deren Vorlagentyp eine implizite Schnittstelle bereitstellt (diesmal nicht abgeleitet von 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;
}

Fall 1 erfordert, dass das übergebene Objekt useCoolClass()ein Kind von CoolClass(und ein Implementierungsobjekt worthless()) ist. Die Fälle 2 und 3 nehmen dagegen jede Klasse an, die eine doSomethingCool()Funktion hat.

Wenn Benutzer des Codes immer eine feine Unterklasse hätten CoolClass, wäre Fall 1 intuitiv sinnvoll, da CoolClassUserimmer eine Implementierung von a erwartet würde CoolClass. Angenommen, dieser Code ist Teil eines API-Frameworks, sodass ich nicht vorhersagen kann, ob Benutzer CoolClassihre eigene Klasse mit einer doSomethingCool()Funktion in Unterklassen unterteilen oder rollen möchten .

Chris Morris
quelle
Vielleicht fehlt mir etwas, aber ist der wichtige Unterschied, der bereits in Ihrem ersten Absatz kurz formuliert wurde, nicht, dass explizite Schnittstellen Laufzeitpolymorphismus sind, während implizite Schnittstellen Polymorphismus zur Kompilierungszeit sind?
Robert Harvey
2
Es gibt einige Probleme, die gelöst werden können, indem entweder eine Klasse oder Funktion vorhanden ist, die einen Zeiger auf eine abstrakte Klasse nimmt (die eine explizite Schnittstelle bereitstellt), oder indem eine Klasse oder Funktion mit Vorlagen vorhanden ist, die ein Objekt verwendet, das eine implizite Schnittstelle bereitstellt. Beide Lösungen funktionieren. Wann möchten Sie die erste Lösung verwenden? Der Zweite?
Chris Morris
Ich denke, die meisten dieser Überlegungen fallen auseinander, wenn Sie Konzepte ein wenig mehr öffnen. Wo würden Sie beispielsweise den statischen Polymorphismus ohne Vererbung einsetzen?
Javier

Antworten:

8

Sie haben bereits den wichtigen Punkt definiert - einer ist die Laufzeit und der andere ist die Kompilierungszeit . Die wirkliche Information, die Sie benötigen, sind die Konsequenzen dieser Wahl.

Kompilierungszeit:

  • Pro: Schnittstellen zur Kompilierungszeit sind viel detaillierter als zur Laufzeit. Damit meine ich, dass Sie nur die Anforderungen einer einzelnen Funktion oder einer Reihe von Funktionen verwenden können, wie Sie sie nennen. Sie müssen nicht immer die gesamte Benutzeroberfläche ausführen. Die Anforderungen sind nur und genau das, was Sie brauchen.
  • Pro: Techniken wie CRTP bedeuten, dass Sie implizite Schnittstellen für Standardimplementierungen von Dingen wie Operatoren verwenden können. Mit der Laufzeitvererbung könnte man so etwas niemals machen.
  • Pro: Implizite Schnittstellen sind viel einfacher zu erstellen und "zu erben" zu multiplizieren als Laufzeitschnittstellen und unterliegen keinerlei binären Einschränkungen. Beispielsweise können POD-Klassen implizite Schnittstellen verwenden. Es besteht keine Notwendigkeit für virtualVererbung oder andere Spielereien mit impliziten Schnittstellen - ein großer Vorteil.
  • Pro: Der Compiler kann weitaus mehr Optimierungen für Schnittstellen zur Kompilierungszeit vornehmen. Darüber hinaus sorgt die zusätzliche Typensicherheit für einen sichereren Code.
  • Pro: Es ist unmöglich, Werte für Laufzeitschnittstellen einzugeben, da Sie die Größe oder Ausrichtung des endgültigen Objekts nicht kennen. Dies bedeutet, dass jeder Fall, der Wertschöpfung benötigt / profitiert, große Vorteile aus Vorlagen zieht.
  • Con: Templates sind eine Hündin zum Kompilieren und Verwenden, und sie können fummelig zwischen Compilern portiert werden
  • Con: Vorlagen können (offensichtlich) nicht zur Laufzeit geladen werden, sodass sie beispielsweise beim Ausdrücken dynamischer Datenstrukturen Grenzen haben.

Laufzeit:

  • Pro: Der endgültige Typ muss erst zur Laufzeit festgelegt werden. Dies bedeutet, dass die Laufzeitvererbung einige Datenstrukturen viel einfacher ausdrücken kann, wenn Vorlagen dies überhaupt können. Sie können auch polymorphe Laufzeit-Typen über C-Grenzen hinweg exportieren, z. B. COM.
  • Pro: Es ist viel einfacher, die Laufzeitvererbung anzugeben und zu implementieren, und Sie erhalten kein wirklich compilerspezifisches Verhalten.
  • Con: Die Laufzeitvererbung kann langsamer sein als die Vererbung zur Kompilierungszeit.
  • Con: Die Laufzeitvererbung verliert Typinformationen.
  • Con: Die Laufzeitvererbung ist viel weniger flexibel.
  • Con: Mehrfachvererbung ist eine Hündin.

Verwenden Sie die relative Liste nicht, wenn Sie keinen bestimmten Vorteil der Laufzeitvererbung benötigen. Es ist langsamer, weniger flexibel und weniger sicher als Vorlagen.

Bearbeiten: Es ist erwähnenswert, dass es insbesondere in C ++ andere Verwendungszwecke als den Laufzeitpolymorphismus gibt. Sie können beispielsweise typedefs erben oder zum Typ-Tagging verwenden oder das CRTP verwenden. Letztendlich fallen diese Techniken (und andere) jedoch wirklich unter "Kompilierungszeit", obwohl sie mit implementiert werden class X : public Y.

DeadMG
quelle
In Bezug auf Ihren ersten Profi für die Kompilierungszeit hängt dies mit einer meiner Hauptfragen zusammen. Möchten Sie jemals klarstellen, dass Sie nur mit einer expliziten Schnittstelle arbeiten möchten? Dh. "Es ist mir egal, ob Sie alle Funktionen haben, die ich benötige. Wenn Sie nicht von Klasse Z erben, möchte ich nichts mit Ihnen zu tun haben." Außerdem verliert die Laufzeitvererbung bei Verwendung von Zeigern / Referenzen keine Typinformationen, richtig?
Chris Morris
@ChrisMorris: Nein. Wenn es funktioniert, dann funktioniert es, was alles ist, was Sie interessieren sollten. Warum sollte jemand genau den gleichen Code an anderer Stelle schreiben?
jmoreno
1
@ ChrisMorris: Nein, würde ich nicht. Wenn ich nur X brauche, dann ist es eines der Grundprinzipien der Kapselung, nach denen ich immer nur fragen und mich um X kümmern sollte. Außerdem verliert es Typinformationen. Sie können beispielsweise ein Objekt eines solchen Typs nicht stapelweise zuordnen. Sie können eine Vorlage nicht mit ihrem wahren Typ instanziieren. Sie können keine Elementfunktionen für Vorlagen aufrufen.
DeadMG
Was ist mit einer Situation, in der Sie eine Klasse Q haben, die eine Klasse verwendet? Q verwendet einen Vorlagenparameter, sodass jede Klasse, die die implizite Schnittstelle bereitstellt, dies tut, denken wir. Es stellt sich heraus, dass die Klasse Q auch erwartet, dass ihre interne Klasse (nennen Sie es H) die Schnittstelle von Q verwendet. Wenn das H-Objekt beispielsweise zerstört wird, sollte es eine Funktion von Qs aufrufen. Dies kann in einer impliziten Schnittstelle nicht angegeben werden. Daher schlagen Vorlagen fehl. Genauer gesagt scheint eine eng gekoppelte Gruppe von Klassen, die mehr als nur implizite Schnittstellen voneinander erfordern, die Verwendung von Vorlagen zu verhindern.
Chris Morris
Con compiletime: Hässlich zu debuggen, Notwendigkeit, die Definitionen in den Header zu setzen
JFFIGK