Ist es in C ++ möglich, eine Klasse als von einer anderen Klasse geerbt weiterzuleiten?

72

Ich weiß, dass ich tun kann:

class Foo;

Aber kann ich eine Klasse als von einer anderen geerbt deklarieren, wie:

class Bar {};

class Foo: public Bar;

Ein beispielhafter Anwendungsfall wären Co-Varianten-Referenzrückgabetypen.

// somewhere.h
class RA {}
class RB : public RA {}

... und dann in einem anderen Header, der irgendwo nicht enthalten ist.h.

// other.h
class RA;

class A {
 public:
  virtual RA* Foo();  // this only needs the forward deceleration
}

class RB : public RA; // invalid but...

class B {
 public:
  virtual RB* Foo();  // 
}

Die einzige Information, die der Compiler benötigen sollte , um die Deklaration von zu verarbeiten RB* B:Foo(), RBist die RAals öffentliche Basisklasse. Jetzt brauchen Sie natürlich irgendwo.h, wenn Sie beabsichtigen, die Rückgabewerte von dereferenzieren Foo. Wenn einige Clients jedoch nie anrufen Foo, gibt es keinen Grund für sie, irgendwo.h einzuschließen, was die Kompilierung erheblich beschleunigen könnte.

anon
quelle
2
@Pace Co-Varianten-Referenzrückgabetypen. (Siehe, bearbeiten.) Aber ich bin nicht sicher, ob Mehrfachvererbungen das nicht durcheinander bringen.
BCS

Antworten:

46

Eine Forward-Deklaration ist nur dann wirklich nützlich, wenn Sie dem Compiler mitteilen möchten, dass eine Klasse mit diesem Namen vorhanden ist und an anderer Stelle deklariert und definiert wird. Sie können es in keinem Fall verwenden, in dem der Compiler Kontextinformationen über die Klasse benötigt, und es ist für den Compiler auch nicht von Nutzen, nur ein wenig über die Klasse zu erzählen. (Im Allgemeinen können Sie die Vorwärtsdeklaration nur verwenden, wenn Sie auf diese Klasse ohne anderen Kontext verweisen, z. B. als Parameter oder Rückgabewert.)

Daher können Sie Bar in keinem Szenario weiterleiten, in dem Sie Foo deklarieren, und es ist absolut nicht sinnvoll, eine Weiterleitungsdeklaration zu haben, die die Basisklasse enthält - was sagt Ihnen das außerdem nichts?

Joe
quelle
66
"Was sagt dir das außer nichts?" Äh, es sagt Ihnen, dass der Typ eine Unterklasse der Basisklasse ist. Das ist nichts Wenn eine Funktion in Ihrer Basisklasse beispielsweise auf ein Objekt stößt, das Ihre Unterklasse enthält (als "anderes" Objekt), kann sie dieses Objekt nicht an eine Funktion übergeben, die ein Basisklassenobjekt verwendet, da sie nicht weiß, wie um die Unterklasse in die Basisklasse umzuwandeln, obwohl sie auf alle öffentlichen Mitglieder der Unterklasse zugreifen kann. Ich stimme Ihnen zu, dass es unmöglich ist, das zu tun, was diese Person verlangt hat, aber ich stimme Ihrer Einschätzung nicht zu, dass es nutzlos wäre, dies zu tun.
Codetaku
@codetaku: Angemeldet, um Ihren Kommentar zu verbessern (: Für alle, die ein konkretes Beispiel suchen, siehe Googles C ++
jwd
2
@codetaku - ja! Sie möchten beispielsweise, dass ein static_assertoder enable_ifein intelligenter Zeiger ihn auf bestimmte Typen beschränkt - Sie möchten den intelligenten Zeiger für einen unvollständigen Typ verwenden ... bam!
Davidbak
1
@jwd - Ich bin mit diesem Google-Codierungsstil nicht einverstanden. Das Verstecken von Informationen ist nützlich, wir wissen es zumindest, seit Parnas es beschrieben hat . Wenn wir nur Adas Paketkörper hätten oder limited private- aber wir tun es nicht. Für die Pimpl-Sprache ist unique_ptrein unvollständiger Typ das Beste, was wir in C ++ tun können.
Davidbak
40

Vorwärtsdeklarationen sind Deklarationen, keine Definitionen. Alles, was die Deklaration einer Klasse erfordert (wie Zeiger auf diese Klasse), benötigt nur die Vorwärtsdeklaration. Alles, was die Definition erfordern würde - dh die tatsächliche Struktur der Klasse kennen müsste - würde jedoch nicht nur mit der Vorwärtsdeklaration funktionieren.

Abgeleitete Klassen müssen auf jeden Fall die Struktur ihres Elternteils kennen, nicht nur, dass das Elternteil existiert, daher wäre eine Vorwärtsdeklaration unzureichend.

Jonathan M Davis
quelle
4
Benötigen Sie die Struktur einer Klasse, um einen Zeiger darauf korrekt zu handhaben? Solange Sie sich ausschließlich mit Referenzen befassen, sollte es Ihnen gut gehen.
BCS
2
@BCS Ja. Solange Sie mit Zeigern arbeiten, benötigen Sie nur Vorwärtsdeklarationen. Wenn Sie jedoch eine abgeleitete Klasse deklarieren, benötigen Sie die Definition des übergeordneten Elements und nicht nur dessen Deklaration, da Sie nicht nur mit Zeigern arbeiten.
Jonathan M Davis
5
Es würde die Implementierung von Objekten einschränken, aber ich denke, es sollte möglich sein, eine *D-> *BKonvertierung durchzuführen, während nur die Vererbungsstruktur bekannt ist. Ich gehe davon aus, dass es so aussieht, als würden die Strukturen zur Implementierung virtueller Basisklassen benötigt.
BCS
21

Nein, es ist nicht möglich, die Vererbung weiterzuleiten, selbst wenn Sie nur mit Zeigern arbeiten. Bei Konvertierungen zwischen Zeigern muss der Compiler manchmal die Details der Klasse kennen, um die Konvertierung korrekt durchzuführen. Dies ist bei Mehrfachvererbung der Fall. (Sie können einige Teile der Hierarchie als Sonderfall verwenden, die nur eine einzelne Vererbung verwenden, aber nicht Teil der Sprache sind.)

Betrachten Sie den folgenden trivialen Fall:

#include <stdio.h>
class A { int x; };
class B { int y; };
class C: public A, public B { int z; };
void main()
{ 
    C c; A *pa = &c; B *pb = &c; C *pc = &c; 
    printf("A: %p, B: %p, C: %p\n", pa, pb, pc);
}

Die Ausgabe, die ich erhalten habe (mit 32 Bit Visual Studio 2010), ist:

A: 0018F748, B: 0018F74C, C: 0018F748

Für die Mehrfachvererbung muss der Compiler beim Konvertieren zwischen verwandten Zeigern eine Zeigerarithmetik einfügen, um die richtigen Konvertierungen zu erzielen.

Aus diesem Grund können Sie die Vererbung nicht weiterleiten, auch wenn Sie nur mit Zeigern arbeiten.

Warum dies nützlich wäre, würde die Kompilierungszeiten verbessern, wenn Sie Co-Varianten-Rückgabetypen anstelle von Casts verwenden möchten. Zum Beispiel wird dies nicht kompiliert:

class RA;
class A             { public: virtual RA *fooRet(); };
class RB;
class B : public A  { public: virtual RB *fooRet(); };

Aber das wird:

class RA;
class A             { public: virtual RA *fooRet(); };
class RA { int x; };
class RB : public RA{ int y; };
class B : public A  { public: virtual RB *fooRet(); };

Dies ist nützlich, wenn Sie Objekte vom Typ B haben (keine Zeiger oder Referenzen). In diesem Fall ist der Compiler intelligent genug, um einen direkten Funktionsaufruf zu verwenden, und Sie können den Rückgabetyp von RB * direkt ohne Casting verwenden. In diesem Fall mache ich normalerweise den Rückgabetyp RA * und mache eine statische Umwandlung des Rückgabewerts.

user1332054
quelle
7
Ich weiß, dass dies eine alte Antwort ist, aber IMO ist dies eine viel hilfreichere Antwort als die höher bewerteten und akzeptierten, die die Tatsache ignorieren, dass die Konvertierung von a Foo*in a Bar*(oder Referenz) selbst mit a nützlich sein kann unvollständiger Typ.
EvanED
1

Alles, was Sie tun mussten, war, RBohne das zu deklarieren : public RA(oh, und auch ;am Ende Ihrer Klassendefinitionen hinzuzufügen ):

class RA;

class A {
    public:
    virtual RA* Foo();
};

class RB;

class B {
public:
    virtual RB* Foo();
};

// client includes somewhere.h
class RA {};
class RB : public RA {};

int main ()
{
    return 0;
}

Dies löst jedoch nicht das spezifische Problem, das in der Antwort von user1332054 gut beschrieben ist.

Einige der anderen Antworten scheinen einige Missverständnisse aufzuzeigen, die ich zerstreuen möchte:

Die Vorwärtsdeklaration ist auch dann nützlich , wenn wir wissen, dass die Definition wahrscheinlich nicht enthalten ist. Dies ermöglicht es uns, in unseren Bibliotheken viele Typabzüge vorzunehmen, die sie mit vielen anderen etablierten Bibliotheken kompatibel machen, ohne sie einzuschließen. Das Einschließen von Bibliotheken führt unnötigerweise zu zu vielen verschachtelten Includes, die die Kompilierungszeit sprengen können. Es wird empfohlen, Ihren Code gegebenenfalls kompatibel zu machen und so wenig wie möglich einzuschließen.

Typischerweise Sie können eine Klasse mit Zeigern auf Klassen definieren , die nur erklärt wurden und nicht definiert. Beispiel:

struct B;

struct A
{
    B * b_;

    B * foo ()
    {
        return b_;
    }

    B & foo (B * b)
    {
        return *b;
    }
};

int main ()
{
    return 0;
}

Das obige Kompilieren funktioniert einwandfrei, da der Compiler nichts über B wissen muss.

Ein Beispiel, bei dem es möglicherweise etwas schwieriger ist zu erkennen, dass der Compiler weitere Informationen benötigt:

struct B;

struct A
{
    B * foo ()
    {
        return new B;
    }
};

Das obige Problem ist, weil new Bder B::B()Konstruktor aufgerufen wird, der noch nicht definiert wurde. Ebenfalls:

struct B;

struct A
{
    void foo (B b) {}
};

Hier foomuss der Kopierkonstruktor für aufgerufen werden b, der ebenfalls noch nicht definiert wurde. Zuletzt:

struct B;

struct A
{
    B b;
};

Hier haben wir implizit Amit dem Standardkonstruktor definiert , der den Standardkonstruktor des Aufrufs seiner Mitglieder aufruft b, der noch nicht definiert wurde. Ich denke, du verstehst, worum es geht.

In Bezug auf das allgemeinere Problem, das von user1332054 beschrieben wird, verstehe ich ehrlich gesagt nicht, warum es nicht möglich ist, Zeiger auf undefinierte Klassen in einer geerbten virtuellen Funktion zu verwenden.

Im weiteren Sinne denke ich jedoch, dass Sie es sich selbst schwerer machen, indem Sie Ihre Klassen definieren , anstatt sie nur zu deklarieren . Hier ist ein Beispiel, in dem Sie DoCleverStuffmit Ihren Klassen in Ihrer Bibliothek beginnen, bevor Sie überhaupt eine Ihrer Klassen definiert haben:

// Just declare

class RA;
class RB;

class A;
class B;

// We'll need some type_traits, so we'll define them:

template <class T>
struct is_type_A
{
    static constexpr bool value = false;
};

template <>
struct is_type_A <A>
{
    static constexpr bool value = true;
};

template <class T>
struct is_type_B
{
    static constexpr bool value = false;
};

template <>
struct is_type_B <B>
{
    static constexpr bool value = true;
};

#include <type_traits>

// With forward declarations, templates and type_traits now we don't
// need the class definitions to prepare useful code:

template<class T>
typename std::enable_if<is_type_A<T>::value, RA *>::type
DoCleverStuff (T & t)
{
    // specific to A

    return t.fooRet();
}

template<class T>
typename std::enable_if<is_type_B<T>::value, RB *>::type
DoCleverStuff (T & t)
{
    // specific to B

    return t.fooRet();
}

// At some point the user *does* the include:

class RA
{
    int x;
};

class RB : public RA
{
    int y;
};

class A
{
public:
    virtual RA * fooRet()
    {
        return new RA;
    }
};

class B : public A
{
public:

    virtual RB * fooRet()
    {
        return new RB;
    }
};

int main ()
{
    // example calls:

    A a;

    RA * ra = DoCleverStuff(a);

    B b;

    RB * rb = DoCleverStuff(b);

    delete ra;
    delete rb;

    return 0;
}
Elliott
quelle
-1

Ich denke nicht, dass es nützlich ist. Bedenken Sie: Sie haben eine Klasse definiert.

class Bar {
public:
    void frob();
};

Jetzt deklarieren Sie eine Klasse Foo:

class Foo;

Mit Foo können Sie lediglich einen Zeiger darauf erstellen. Angenommen, Sie fügen die Informationen hinzu, Foodie abgeleitet sind von Bar:

class Foo: public Bar;

Was können Sie jetzt tun, was Sie vorher nicht konnten? Ich denke, Sie können nur einen Zeiger akzeptieren Foound in einen Zeiger umwandeln Barund dann diesen Zeiger verwenden.

void frob(Foo* f) {
    Bar *b = (Bar)f;
    b->frob();
}

Sie müssen den Zeiger jedoch an einer anderen Stelle generiert haben, sodass Sie Barstattdessen einfach einen Zeiger auf akzeptieren könnten .

void frob(Bar* b) {
    b->frob();
}
Andrew Aylett
quelle
2
Klasse Foo: öffentliche Bar; sagt, dass dynamic_cast <> funktioniert, nicht wahr? Ich denke, das wäre eine wichtige Information.
Fabian