893 Stimmen

Wie deklariert man eine Schnittstelle in C++?

Wie kann ich eine Klasse einrichten, die eine Schnittstelle darstellt? Ist dies nur eine abstrakte Basisklasse?

33voto

Rexxar Punkte 1838

Meine Antwort ist im Grunde dieselbe wie die der anderen, aber ich denke, es gibt noch zwei weitere wichtige Dinge zu tun:

  1. Deklarieren Sie einen virtuellen Destruktor in Ihrer Schnittstelle oder erstellen Sie einen geschützten nicht-virtuellen Destruktor, um undefiniertes Verhalten zu vermeiden, wenn jemand versucht, ein Objekt des Typs IDemo .

  2. Verwenden Sie die virtuelle Vererbung, um Probleme bei der Mehrfachvererbung zu vermeiden. (Bei der Verwendung von Schnittstellen kommt es häufiger zu Mehrfachvererbung).

Und wie andere Antworten:

  • Erstellen Sie eine Klasse mit rein virtuellen Methoden.
  • Verwenden Sie die Schnittstelle, indem Sie eine andere Klasse erstellen, die diese virtuellen Methoden außer Kraft setzt.

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
            virtual ~IDemo() {}
    }

    O

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
        protected:
            ~IDemo() {}
    }

    Und

    class Child : virtual public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    }

2 Stimmen

Ist eine virtuelle Vererbung nicht erforderlich, da Sie keine Datenelemente in einer Schnittstelle haben.

3 Stimmen

Die virtuelle Vererbung ist auch für Methoden wichtig. Ohne sie werden Sie mit OverrideMe() auf Unklarheiten stoßen, selbst wenn eine der "Instanzen" davon rein virtuell ist (ich habe das gerade selbst ausprobiert).

6 Stimmen

@Avishay_ " ist eine virtuelle Vererbung nicht erforderlich, da Sie keine Datenelemente in einer Schnittstelle haben. " Falsch.

11voto

gnzlbg Punkte 6837

In C++11 können Sie die Vererbung leicht ganz vermeiden:

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

In diesem Fall hat eine Schnittstelle Referenzsemantik, d.h. man muss sicherstellen, dass das Objekt die Schnittstelle überlebt (es ist auch möglich, Schnittstellen mit Wertsemantik zu erstellen).

Diese Arten von Schnittstellen haben ihre Vor- und Nachteile:

Schließlich ist die Vererbung die Wurzel allen Übels bei der Entwicklung komplexer Software. Unter Sean Parents Wertesemantik und konzeptbasierte Polymorphie (sehr empfehlenswert, dort werden bessere Versionen dieser Technik erläutert) wird der folgende Fall untersucht:

Angenommen, ich habe eine Anwendung, in der ich meine Formen polymorph mit der MyShape Schnittstelle:

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

In Ihrer Anwendung tun Sie dasselbe mit verschiedenen Formen, indem Sie die YourShape Schnittstelle:

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

Nehmen wir nun an, Sie möchten einige der von mir entwickelten Formen in Ihrer Anwendung verwenden. Vom Konzept her haben unsere Formen die gleiche Schnittstelle, aber damit meine Formen in Ihrer Anwendung funktionieren, müssten Sie meine Formen wie folgt erweitern:

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

Erstens ist es vielleicht gar nicht möglich, meine Formen zu ändern. Außerdem führt die Mehrfachvererbung zu Spaghetti-Code (stellen Sie sich vor, es kommt ein drittes Projekt hinzu, das die TheirShape Schnittstelle... was passiert, wenn sie auch ihre Zeichenfunktion aufrufen my_draw ?).

Update: Es gibt ein paar neue Referenzen über Polymorphismus, der nicht auf Vererbung basiert:

5 Stimmen

Ehrlich gesagt ist Vererbung viel klarer als dieses C++11-Ding, das vorgibt, eine Schnittstelle zu sein, aber eher ein Klebstoff ist, um einige inkonsistente Designs zu verbinden. Das Beispiel mit den Formen ist von der Realität losgelöst und Circle Klasse ist ein schlechtes Design. Sie sollten verwenden Adapter Muster in solchen Fällen. Es tut mir leid, wenn es ein wenig hart klingt, aber versuchen Sie, eine Bibliothek aus dem wirklichen Leben zu verwenden, wie Qt bevor sie über die Vererbung urteilen. Erben macht das Leben viel einfacher.

2 Stimmen

Das klingt überhaupt nicht hart. Inwiefern ist das Beispiel der Form von der Realität abgekoppelt? Könnten Sie ein Beispiel (vielleicht auf ideone) für die Fixierung eines Kreises mit Hilfe des Adapter Muster? Ich bin daran interessiert, seine Vorteile zu sehen.

0 Stimmen

Okay, ich werde versuchen, in diesen kleinen Kasten zu passen. Zunächst einmal wählt man normalerweise Bibliotheken wie "MyShape" aus, bevor man mit dem Schreiben einer eigenen Anwendung beginnt, um seine Arbeit zu sichern. Wie könnten Sie sonst wissen Square nicht bereits vorhanden ist? Vorauswissen? Das ist der Grund, warum es von der Realität losgelöst ist. Und in der Realität, wenn Sie sich für die Bibliothek "MyShape" entscheiden, können Sie sich von Anfang an an deren Schnittstelle anpassen. In Shapes Beispiel gibt es viele Unsinnigkeiten (eine davon ist, dass Sie zwei Circle structs), aber der Adapter würde etwa so aussehen -> ideone.com/UogjWk

10voto

Rodyland Punkte 528

Alle oben genannten Antworten sind gut. Eine zusätzliche Sache, die Sie im Auge behalten sollten - Sie können auch einen rein virtuellen Destruktor haben. Der einzige Unterschied ist, dass Sie ihn noch implementieren müssen.

Verwirrt?

    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

Der Hauptgrund, warum Sie dies tun sollten, ist, wenn Sie, wie ich, Schnittstellenmethoden bereitstellen möchten, deren Überschreibung aber optional ist.

Um die Klasse zu einer Schnittstellenklasse zu machen, ist eine rein virtuelle Methode erforderlich, aber alle Ihre virtuellen Methoden haben Standardimplementierungen, so dass die einzige Methode, die noch rein virtuell gemacht werden muss, der Destruktor ist.

Die Reimplementierung eines Destruktors in der abgeleiteten Klasse ist überhaupt keine große Sache - ich reimplementiere immer einen Destruktor, ob virtuell oder nicht, in meinen abgeleiteten Klassen.

5 Stimmen

Warum, oh warum, sollte jemand den dtor in diesem Fall rein virtuell machen wollen? Was wäre der Vorteil davon? Man würde den abgeleiteten Klassen nur etwas aufzwingen, das sie wahrscheinlich nicht brauchen - einen dtor.

7 Stimmen

Ich habe meine Antwort aktualisiert, um Ihre Frage zu beantworten. Ein rein virtueller Destruktor ist ein gültiger Weg, um eine Schnittstellenklasse zu erreichen (der einzige Weg?), bei der alle Methoden Standardimplementierungen haben.

9voto

Luc Hermitte Punkte 30868

Sie können auch Vertragsklassen in Betracht ziehen, die mit dem NVI (Non Virtual Interface Pattern) implementiert werden. Zum Beispiel:

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1() = default;
    void f(Parameters p) {
        assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
        // + class invariants.
        do_f(p);
        // Check post-conditions + class invariants.
    }
private:
    virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
    void do_f(Parameters p) override; // From contract 1.
    void do_g(Parameters p) override; // From contract 2.
};

3 Stimmen

Für andere Leser ist dies Dr. Dobbs Artikel "Gespräche: Virtually Yours" von Jim Hyslop und Herb Sutter geht etwas näher darauf ein, warum man den NVI verwenden sollte.

3 Stimmen

Und auch dieser Artikel "Virtualität" von Herb Sutter.

0 Stimmen

Die "Gespräche: Virtually Yours" ist heutzutage schwer zu googeln, selbst die Verweise im Virtualität Artikel ist tot. Danke für den Link @user2067021

7voto

Mark Ingram Punkte 68414

Wenn Sie den C++-Compiler von Microsoft verwenden, können Sie wie folgt vorgehen:

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

Ich mag diesen Ansatz, weil er zu einem viel kleineren Schnittstellencode führt und die Größe des generierten Codes deutlich geringer sein kann. Die Verwendung von novtable entfernt alle Verweise auf den vtable-Zeiger in dieser Klasse, so dass Sie ihn niemals direkt instanziieren können. Siehe die Dokumentation hier - novable .

4 Stimmen

Ich verstehe nicht ganz, warum Sie den Begriff novtable über Standard virtual void Bar() = 0;

2 Stimmen

Es ist zusätzlich zu (ich habe gerade die fehlende = 0; die ich hinzugefügt habe). Lesen Sie die Dokumentation, wenn Sie es nicht verstehen.

0 Stimmen

Ich habe es ohne den = 0; und nahm an, dass es nur eine nicht standardisierte Art und Weise war, genau das Gleiche zu tun.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X