2009-03-06 10 views
7

Welche Beziehung besteht zwischen der Verwendung virtueller Funktionen und C++ - Vererbungsmechanismen im Vergleich zur Verwendung von Vorlagen und Boost-Konzepten?C++ - Konzeptprüfung vs Vererbung

Es scheint, als ob es eine Überlappung dessen gibt, was möglich ist. Es scheint nämlich möglich zu sein, polymorphes Verhalten mit jedem Ansatz zu erreichen. Wann ist es also sinnvoll, einander zu bevorzugen?

Der Grund, warum ich dies aufbringe, ist, weil ich einen Vorlagenbehälter habe, in dem die Behälter selbst eine hierarchische Beziehung haben. Ich möchte Algorithmen schreiben, die diese Container verwenden, ohne sich darum zu kümmern, um welchen speziellen Container es sich handelt. Außerdem würden einige Algorithmen von dem Wissen profitieren, dass der Schablonentyp bestimmten Konzepten (z. B. vergleichbar) entspricht.

Also auf der einen Seite möchte ich Container polymorph zu verhalten. Auf der anderen Seite muss ich immer noch Konzepte verwenden, wenn ich einige Algorithmen korrekt implementieren möchte. Was muss ein Junior-Entwickler tun?

Antwort

6

Ich denke an Konzepte als eine Art Meta-Interface. Sie kategorisieren Typen nach ihren Fähigkeiten. Die nächste C++ - Version liefert native Konzepte. Ich hatte es nicht verstanden, bis ich auf die Konzepte von C++ 1x stieß und wie sie es ermöglichen, verschiedene, aber nicht miteinander verwandte Typen zusammenzubringen. Stellen Sie sich vor, Sie haben eine Range Schnittstelle. Sie können das auf zwei Arten modellieren. Eines ist ein Subtyp Beziehung:

class Range { 
    virtual Iterator * begin() = 0; 
    virtual Iterator * end() = 0; 

    virtual size_t size() = 0; 
}; 

Natürlich jede Klasse, die von diesem leitet die Range-Schnittstelle implementiert und kann mit Ihren Funktionen verwendet werden. Aber jetzt siehst du, dass es begrenzt ist. Was ist mit einem Array? Es ist auch eine Reihe!

T t[N]; 

begin() => t 
end() => t + size() 
size() => N 

Leider können Sie kein Array von dieser Range-Klasse ableiten, die diese Schnittstelle implementiert.Sie benötigen eine zusätzliche Methode (überladen). Und was ist mit Containern von Drittanbietern? Ein Benutzer Ihrer Bibliothek möchte möglicherweise ihre Container zusammen mit Ihren Funktionen verwenden. Aber er kann die Definition ihrer Container nicht ändern. Hier Konzepte kommen in Spiel:

auto concept Range<typename T> { 
    typename iterator; 
    iterator T::begin(); 
    iterator T::end(); 
    size_t T::size(); 
} 

Nun, sagen Sie etwas über die unterstützten Operationen eines Typs, die erfüllt werden kann, wenn T die entsprechenden Mitgliedsfunktionen. In Ihrer Bibliothek würden Sie die Funktion generisch schreiben. Auf diese Weise können Sie jede Art akzeptieren solange es die erforderlichen Operationen unterstützt:

template<Range R> 
void assign(R const& r) { 
    ... iterate from r.begin() to r.end(). 
} 

Es ist eine großartige Art von Ersetzbarkeit. Jeder Typ passt zu der Rechnung, die dem Konzept entspricht, und nicht nur zu den Typen, die aktiv eine Schnittstelle implementieren. Die nächste C++ Standard-geht weiter: Es definiert ein Container Konzept, das durch reines Arrays passen wird (durch etwas caled Konzept Karte die definiert, wie eine Art etwas Konzept paßt) und andere, bestehende Standardcontainer.

Der Grund, warum ich das oben bringen, weil ich einen Templat-Behälter haben, wo die Behälter selbst eine hierarchische Beziehung haben. Ich möchte Algorithmen schreiben, die diese Container verwenden, ohne sich darum zu kümmern, um welchen speziellen Container es sich handelt. Außerdem würden einige Algorithmen von dem Wissen profitieren, dass der Schablonentyp bestimmten Konzepten (z. B. vergleichbar) entspricht.

Sie können beide mit Vorlagen tatsächlich tun. Sie können Ihre hierarchische Beziehung weiterhin verwenden, um Code freizugeben, und dann die Algorithmen in generischer Form schreiben. Zum Beispiel, um zu kommunizieren, dass Ihr Container vergleichbar ist. Das ist wie Standard-Random-Access/Vorwärts/Ausgang/Eingang Iteratorkategorien sind implementiert:

// tag types for the comparator cagetory 
struct not_comparable { }; 
struct basic_comparable : not_comparable { }; 

template<typename T> 
class MyVector : public BasicContainer<T> { 
    typedef basic_comparable comparator_kind; 
}; 

/* Container concept */ 
T::comparator_kind: comparator category 

Es ist eine vernünftige einfache Art und Weise, es zu tun, eigentlich. Jetzt können Sie eine Funktion aufrufen und sie wird zur korrekten Implementierung weitergeleitet.

template<typename Container> 
void takesAdvantage(Container const& c) { 
    takesAdvantageOfCompare(c, typename Container::comparator_kind()); 
} 

// implementation for basic_comparable containers 
template<typename Container> 
void takesAdvantage(Container const& c, basic_comparable) { 
    ... 
} 

// implementation for not_comparable containers 
template<typename Container> 
void takesAdvantage(Container const& c, not_comparable) { 
    ... 
} 

Es gibt tatsächlich verschiedene Techniken, die verwendet werden können, um das zu implementieren. Eine andere Möglichkeit besteht darin, mit boost::enable_if verschiedene Implementierungen jedes Mal zu aktivieren oder zu deaktivieren.

+0

C++ 1x? Bedeutet das, dass sie es aufgegeben haben, den neuen Standard in diesem Jahrzehnt zu veröffentlichen, oder sprechen Sie von der zukünftigen C++ - Entwicklung? – jpalecek

+0

http://www.research.att.com/~bs/C++0xFAQ.html#concepts – jmucchiello

+0

jpalecek, sie wollen es im Jahr 2010 veröffentlichen. Ich habe die Angewohnheit, es C++ 1x zu nennen :) –

0

Wenn eine Entscheidung zur Kompilierungszeit getroffen werden kann, verwenden Sie Vorlagen. Ansonsten verwenden Sie Vererbung und virtuelle Funktionen.

1

Ja, polymorphes Verhalten ist mit beiden Mechanismen möglich. In der Tat sind beide Polymorphismus auch genannt.

Virtuelle Funktionen geben Ihnen dynamischen Polymorphismus (weil es zur Laufzeit entschieden wird), während Templates Ihnen statischen Polymorphismus geben (alles wird zur Kompilierungszeit entschieden).

Und das sollte die Frage beantworten, welche zu bevorzugen ist. Wenn möglich, ziehen Sie es vor, die Arbeit zur Kompilierungszeit zu verschieben. Also, wenn Sie damit durchkommen können, verwenden Sie Vorlagen, um Ihre Polymorphie Bedürfnisse zu lösen. Und wenn das nicht möglich ist (weil Sie Informationen zum Laufzeittyp verwenden müssen, da die genauen Typen zum Zeitpunkt der Kompilierung nicht bekannt sind), greifen Sie auf den dynamischen Polymorphismus zurück.

(Natürlich kann es andere Gründe geben, das eine oder das andere zu bevorzugen. Insbesondere erfordern Vorlagen, dass Sie eine Menge Code in Headerdateien verschieben, was ein Problem sein kann oder auch nicht, und die Übersetzungsgeschwindigkeit leidet darunter. die auch oder kein Problem sein können.)

0

In diesem speziellen Fall, dass Sie so etwas wie

template<typename T> 
class ContainerBase{}; 

template<typename T> 
class ContainerDerived : public ContainerBase<T> {}; 

Da jede ‚Container‘ Art einzigartig ist für jeden Vorlagentyp tun, gibt es keine Grund Elementfunktionen Jeder Containertyp konnte nicht auf die Eigenschaften des Templates spezialisiert werden.

0

Als einfaches Beispiel für den Unterschied zwischen Kompilierung-und Laufzeit-Polymorphismus betrachtet den folgenden Code:

template<typename tType> 
struct compileTimePolymorphism 
{ }; 

// compile time polymorphism, 
// you can describe a behavior on some object type 
// through the template, but you cannot interchange 
// the templates 
compileTimePolymorphism<int> l_intTemplate; 
compileTimePolymorphism<float> l_floatTemplate; 
compileTimePolymorphism *l_templatePointer; // ???? impossible 

struct A {}; 
struct B : public A{}; 
struct C : public A{}; 

// runtime polymorphism 
// you can interchange objects of different type 
// by treating them like the parent 
B l_B; 
C l_C: 
A *l_A = &l_B; 
l_A = &l_C; 

Compile-Zeit Polymorphismus ist eine gute Lösung, wenn das Verhalten eines Objekts auf einem anderen abhängt Objekt. Laufzeit-Polymorphismus ist notwendig, wenn das Verhalten eines Objekts geändert werden muss.

Die beiden können durch die Definition einer Vorlage kombiniert werden, die polymorph:

template<typename tType> 
struct myContainer : public tType 
{}; 

Die Frage ist dann, wenn das Verhalten des Containers (Runtime-Polymorphismus) geändert werden muss, und wo das Verhalten der Objekte abhängt Es enthält (Kompilierzeit Polymorphismus).