Modernes OOP unter C++
-
Vielen Dank für Eure Ratschläge
Ich werde mir die von Euch vorgeschlagenen Quellen anschauen. Über weitere Quellen werde ich mich sehr freuen.Simon2 schrieb:
Konrad Rudolph schrieb:
...Z.B. könnte man sich eine abstrakte Basisklasse (oder Interface) namens 'Range' vorstellen, welche die Methode 'Sort' definiert und von der alle sortierbaren Containertypen erben. ...
Was sollte das bringen ?
Inwieweit profitiert man von einer derart starken Kopplung zwischen Containern und Algorithmen ?
Ich finde es deutlich sinnvoller (und "sauberer" - was immer man darunter verstehen mag), wenn Container nur ein "Zugriffsinterface" für seine Elemente zur Verfügung stellen, das alle Anwender (und dazu zählt dann auch ein sort) nutzen können. Ein Container hat IMHO genau eine Aufgabe: Elemente verwalten - und mehr sollte er gar nicht können.Hmm... da bin ich der gleichen Meinung, dass man Algos vom Zugriffsinterface trennen sollte. Mit einem Algo bezeichnet man aber IMO die Implementierung und nicht den Bezeichner. Wenn ein Container/Collection jedoch das Interface (z.B.) "Sortable" bereitstellt, dann ist das eine Eigenschaft! Ich weiß nun, dass dieser Container sortierbar ist. (Was man sich in der STL mühsam zusammenlesen muss).
Dies hat noch einen weiteren Vorteil: Polymorphie. Ohne das sind moderne Design-Patterns kaum denkbar.
Fallbeispiel:
Was interessiert mich denn, ob eine (bestimmte) Methode X eine einfach verkettete Liste, eine doppelt v. Liste oder einen AVL-Baum zurückgibt? Was ich ledeglich brauche ist ein "Sortable" und ein "Iterable". Die Implementierung der Methode X braucht mich nicht zu interessieren. Dammit lassen sich sehr schöne Entwurfsmusster entwickeln
mfg
PS: Nein, das ist kein "Java vs C++" Thread. Bitte last es nicht zu einem solchen ausarten. Hier werden ledeglich Designetscheidungen disskutiert.
-
linu(x)bie schrieb:
Dies hat noch einen weiteren Vorteil: Polymorphie. Ohne das sind moderne Design-Patterns kaum denkbar.
Da hast Du die C++-Algorithmen gründlich missverstanden. Die sind nämlich polymorph, um genau zu sein sind sie sogar *wesentlich* weniger gekoppelt als in Java. Nur handelt es sich eben um Compilezeit-Polymorphie, was aber auch in fast allen (> 99%, würde ich schätzen) Fällen vollkommen ausreichend ist. Dass man ein fundamentales Verhalten zur Laufzeit ändert, ist doch schon ein sehr seltener Fall.
Was interessiert mich denn, ob eine (bestimmte) Methode X eine einfach verkettete Liste, eine doppelt v. Liste oder einen AVL-Baum zurückgibt?
Eben. Und genau in dieser Hinsicht ist C++ sogar weiter als Java. Java verlangt seinen Algorithmen ab, auf irgendeinen Container zuzugreifen. Die Algorithmen in C++ scheren sich nicht um Container, stattdessen arbeiten alle C++-Algorithmen mit einem beliebigen offenen Intervall [a, b[. Das ist wesentlich allgemeiner.
-
linu(x)bie schrieb:
Hmm... da bin ich der gleichen Meinung, dass man Algos vom Zugriffsinterface trennen sollte. Mit einem Algo bezeichnet man aber IMO die Implementierung und nicht den Bezeichner. Wenn ein Container/Collection jedoch das Interface (z.B.) "Sortable" bereitstellt, dann ist das eine Eigenschaft! Ich weiß nun, dass dieser Container sortierbar ist. (Was man sich in der STL mühsam zusammenlesen muss).
Für mich ist etwas wie ein Interface auch eine feste Kopplung. Du bist gezwungen die Implementierung in der Klasse zu machen, die STL entkoppelt dies durch Trennung der Interatoren von den Containern.
Der Typ des Iterators eines Containers gibt an was damit gemacht werden kann, und erlaubt es auch einen Algorithmus nachträglich durch einen Spezialisierten auszutauschen ohne das Interface des Containers ergänzen zu können. Und sei es durch eine Templatespezialisierung eines vorhandenen Algorithmus.
Ich kann beide Ansätze gut verstehen, sowohl den Ansatz mit Interfaces (Laufzeitpolymorphie) als auch der Ansatz der STL (Compilezeitpolymorphie). Letzteres dürfte dir aus Java gänzlich unbekannt sein. Beide haben ihren Sinn und beide haben ihre Vor- und Nachteile. Die STL ist z.B. besonders stark Richtung Effizienz ausgelegt, dafür erfolgt die Bindung zur Compilezeit.
linu(x)bie schrieb:
Dies hat noch einen weiteren Vorteil: Polymorphie. Ohne das sind moderne Design-Patterns kaum denkbar.
Hier stößt du auf eine Wissensgrenze wie oben erwähnt. Interfaces, abstrakte Basisklassen, virtuelle Funktionen etc. erlauben Polymorphie. So weit ganz klar. Aber die Generische Programmierung über Templates erlauben auch Polymorphie. Die eine Erfolgt zur Laufzeit die andere zur Compilezeit.
Nehmen wir mal folgendes an:
template<typename T> void foo(const T& obj) { obj->foo(); }Sofern ein Typ T eine Methode foo besitzt die Konstant ist, lässt sich dies auflösen. In der Laufzeitpolymorphie müsste dazu eine Basisklasse/Interface festgelegt sein, in der Compilezeitpolymorphie muss der Ausdruck auf den entsprechenden Typ anwendbar sein (unabhängig von Klassenhierachien). Sollte letzteres nicht gegeben sein, tritt ein Fehler zur Compilezeit auf.
Ich nehme deinen Satz mal und Zweckentfremde ihn hier gehässigerweise:
linu(x)bie schrieb:
...Dammit lassen sich sehr schöne Entwurfsmusster entwickeln :)...
Mit Templates lässt sich noch weit mehr machen (Berechnungen über Typen; Metaprogrammierung...) aber für jetzt sollte dieses kleine Beispiel besser zu dem von dir aufgeworfenen Punkt passen.
linu(x)bie schrieb:
PS: Nein, das ist kein "Java vs C++" Thread. Bitte last es nicht zu einem solchen ausarten. Hier werden ledeglich Designetscheidungen disskutiert.
Ich sehe hier keinen der bislang gegen Java gesprochen hat. Die Designziele sind unterschiedlich und die eine Sprache hat ihre Stärken in dem einen, die andere in dem anderen Anwendungsfall. Nur muss man die Unterschiede klarstellen um sie zu verstehen.
cu André
-
Dieses Bild sollte deutlich genug sein, um zu zeigen, was das Ziel war, das die Container eben kein Sortable-Interface haben sollten:
http://www.kharchi.eu/wiki/lib/exe/fetch.php?cache=cache&media=cpp:std:iterator.pngIch kann sogar ein primitives C-Array durch std::sort sortieren oder durch std::search durchsuchen lassen. Das C-Arraay kann aber bekanntlich keine Interfaces haben.
Ich finde das ziemlich cool! 
Achja, was ist wenn ich in einem Container suchen lassen will? Brauche ich dann ein Findable-Interface? Wieviele Interfaces soll ich am Ende auf einem Container implementieren? Wird doch etwas viel, oder?
Durch die Trennung kann ich eine Fülle an Algorithmen auf (fast) beliebige Container-Typen anwenden. Akademisch gesehen, greifen die Algos ja nicht mal auf Container zu, sondern nur auf Iteratoren. Sollte man beachten, wenn man Haarspalterei betreiben will.

-
Hi Konrad,
vielen Dank für die ausführliche Antwort. Jetzt weiß ich ein wenig mehr, worauf Du hinauswillst. Teilweise sehe ich das auch so, teilweise empfinde ich doch eine "Vererbungskopplung" als zu stark - aber nun gut.
Konrad Rudolph schrieb:
...
Das ist IMHO ein sehr gleitender Übergang.
'size' muss eine Memberfunktion sein.
'empty' auch? Nein, denn es kann über 'size' implementiert werden.
Trotzdem besitzt jeder Container diese Funktion.
...- Stimmt
- stimmt
- stimmt
- stimmt - finde ich aber schade.Konrad Rudolph schrieb:
...
Was genau gefällt Dir daran nicht?Verschiedenes (z.B. die verschiedenen "char*-Einspränkelungen" - aber da hätte man wohl einiges mehr an der Sprache umbauen müssen) aber besonders, dass es eine (viel zu) "fette Klasse" ist.
Gruß,
Simon2.
-
Artchi schrieb:
Ich kann sogar ein primitives C-Array durch std::sort sortieren oder durch std::search durchsuchen lassen. Das C-Arraay kann aber bekanntlich keine Interfaces haben.

Eben. Und deswegen ist die in C++ benutzte Lösung für C++ auch die beste. Aber Stroustrup hat selbst verschiedentlich gesagt, dass er C++ ganz anders designed hätte, wenn die C-Altlast nicht wäre. In einer Sprache, die keine Rücksicht auf sowas wie primitive Datentypen nehmen muss, kann man anders herangehen.
Achja, was ist wenn ich in einem Container suchen lassen will? Brauche ich dann ein Findable-Interface? Wieviele Interfaces soll ich am Ende auf einem Container implementieren? Wird doch etwas viel, oder?
Was ist „etwas viel“? Was spricht dagegen, wenn ein Container viele Konzepte implementiert? Das ist doch zur Zeit auch schon der Fall (schau Dir mal die SGI-Dokumentation der STL an!), nur geschieht das noch nicht explizit im Code.
-
Simon2 schrieb:
Konrad Rudolph schrieb:
...
Das ist IMHO ein sehr gleitender Übergang.
'size' muss eine Memberfunktion sein.
'empty' auch? Nein, denn es kann über 'size' implementiert werden.
Trotzdem besitzt jeder Container diese Funktion.
...- Stimmt
- stimmt
- stimmt
- stimmt - finde ich aber schade.ich find's gut. So kann ich zum beispiel in konstanter Zeit überprüfen, ob eine Liste leer ist, wohingegen der size-Aufruf lineare Zeit kosten darf (und es imho bei einer brauchbaren Implementierung auch tun sollte).
-
Konrad Rudolph schrieb:
Was ist „etwas viel“? Was spricht dagegen, wenn ein Container viele Konzepte implementiert?
http://www.cplusplus.com/reference/algorithm/ << Das ist die Liste der std::algorythmen - und die verdient durchaus die Bezeichnung "etwas viel" Wenn jeder Container von einer entsprechenden Basisklasse erben wuerde, waere das ein wahrer Vererbungsdschungel. Stell dir mal vor du implementierst selbst einen Container, und parallel dazu einen Algorithmus mit dem du auf beliebigen Containern etwas machen moechtest, was in den Standardalgorithmen noch nicht drin ist. Fuer letzteren musst du alle bereits vorhandenen Containerdefinitionen die du nutzt, abaendern, damit sie von deinem neuen Interface erben, und der Container muss von allem erben was in Betracht kommt fuer eine spaetere Benutzung. (Und wenn er fuer was benutzt wird was du nicht in Betracht gezogen hast viel spass beim neucompilieren des dann geaenderten Containers.
Da nimmt man doch lieber die Plug&Play-STL die man nach belieben erweitern kann, auch wenns fuer die Vertreter der "OOP-muss-Vererbung-sein!"-Lobby kein OOP ist.
-
pumuckl schrieb:
Konrad Rudolph schrieb:
Was ist „etwas viel“? Was spricht dagegen, wenn ein Container viele Konzepte implementiert?
http://www.cplusplus.com/reference/algorithm/ << Das ist die Liste der std::algorythmen - und die verdient durchaus die Bezeichnung "etwas viel" Wenn jeder Container von einer entsprechenden Basisklasse erben wuerde, waere das ein wahrer Vererbungsdschungel.
Auch nicht schlimmer als diese Liste. Es ist ja kein Unterschied, ob's freie oder gebundene Funktionen sind.
Stell dir mal vor du implementierst selbst einen Container, und parallel dazu einen Algorithmus mit dem du auf beliebigen Containern etwas machen moechtest, was in den Standardalgorithmen noch nicht drin ist. Fuer letzteren musst du alle bereits vorhandenen Containerdefinitionen die du nutzt, abaendern, damit sie von deinem neuen Interface erben, und der Container muss von allem erben was in Betracht kommt fuer eine spaetere Benutzung.
Nein, eben nicht. Sondern Du würdest (wieder in einer imaginären Sprache) folgendes schreiben:
trait MeinTollerNeuerContainerTrait[T as Printable] function MeineTollemethode() # Alle Elemente ausgeben: for element in this Console.WriteLine(element) end end trait Range is MeinTollerNeuerContainerTrait– Man braucht quasi die Möglichkeit, „Mix-Ins“ zu definieren. Mehr nicht.
Da nimmt man doch lieber die Plug&Play-STL die man nach belieben erweitern kann
Nochmal: Was mir vorschwebt, ist semantisch dasselbe wie die STL-Algorithmen (wenn man vom Methoden-Lookup-Algorithmus absieht), nur die Syntax ist anders, um die Zugehörigkeit besser widerzuspiegeln.
-
Konrad Rudolph schrieb:
Nochmal: Was mir vorschwebt, ist semantisch dasselbe wie die STL-Algorithmen (wenn man vom Methoden-Lookup-Algorithmus absieht), nur die Syntax ist anders, um die Zugehörigkeit besser widerzuspiegeln.
Wobei das Thema "Modernes OOP unter C++" und nicht einer anderen Sprache ist. Der Fragesteller wollte wohl wissen warum die Designentscheidung so und nicht anders in C++ getroffen wurde, als zu wissen wie du es mit anderen Mitteln umsetzen würdest ;p
cu André
-
asc schrieb:
Wobei das Thema "Modernes OOP unter C++" und nicht einer anderen Sprache ist. Der Fragesteller wollte wohl wissen warum die Designentscheidung so und nicht anders in C++ getroffen wurde, als zu wissen wie du es mit anderen Mitteln umsetzen würdest ;p
Stimmt, ich bin abgedriftet. Aber wie ich vor einigen Tagen schon in einem anderen Thread sagte: So ist das nunmal in Diskussionen. Ohne solche Diskussionen wäre das Forum doch wesentlich weniger wert.
-
Konrad Rudolph schrieb:
...
Das ist IMHO ein sehr gleitender Übergang.
'size' muss eine Memberfunktion sein.
'empty' auch? Nein, denn es kann über 'size' implementiert werden.
Trotzdem besitzt jeder Container diese Funktion.
...- ja.
- Warum? Der gleiche Fehlschluss stört mich auch bei Sutters ansonsten excellenter Analyse von basic_string. Was hat die Wahl des Interfaces mit der Implementierbarkeit der Funktionalität zu tun? Oder bei Sutter - warum sollte es den Anwender interessieren, welcher der verschiedenen Overloads nun der Allgemeinste ist (der kriegt die Membersyntax)
- nicht unter den Komplexitätsbedingungen, die an empty gestellt werden (list), allerdings ist empty als begin()==end() implementierbar
-
camper schrieb:
Konrad Rudolph schrieb:
'size' muss eine Memberfunktion sein.
- Warum? Der gleiche Fehlschluss stört mich auch bei Sutters ansonsten excellenter Analyse von basic_string. Was hat die Wahl des Interfaces mit der Implementierbarkeit der Funktionalität zu tun? Oder bei Sutter - warum sollte es den Anwender interessieren, welcher der verschiedenen Overloads nun der Allgemeinste ist (der kriegt die Membersyntax)
So argumentiere ich ja gar nicht. Andererseits: Wie würdest Du es machen? 'size' als Algorithmus implementieren? Denkbar. Aber was wären die Vorteile?
Mir ging es ja in meiner Argumentation darum, dass ich die Unterscheidung grundsätzlich für ziemlich willkürlich halte. Eine Schnittstelle ist für mich konzeptuell das, was man mit einer Klasse (bzw. Exemplaren davon) machen kann.
-
linu(x)bie schrieb:
Wenn ein Container/Collection jedoch das Interface (z.B.) "Sortable" bereitstellt, dann ist das eine Eigenschaft!
Eine Sequenz mit Objekten vom Typ T ist genau dann sortiertbar, wenn T eine Ordnungsrelation (hier operator<) besitzt. Daher ist die Eigenschaft sortierbar keine des Sequenzcontainers, sondern sie ist eine der Elemente.
Ich weiß nun, dass dieser Container sortierbar ist. (Was man sich in der STL mühsam zusammenlesen muss).
Das ist überflüssig, das weiß man schon vorher. Die Eigenschaft hängt nur von Deinen Elementen ab und nicht vom Container!
Beispiel:
#include <vector> #include <algorithm> class A { int i_; public: A(int const i) : i_(i) {} }; class B { int i_; public: B(int const i) : i_(i) {} bool less (B const& rhs) const { return this->i_ < rhs.i_; } }; bool operator< (B const& lhs, B const& rhs) { return lhs.less(rhs); } int main () { std::vector<A> a; std::vector<B> b; for (size_t i = 100; i != 0; --i) { a.push_back(A(i)); b.push_back(B(i)); } std::sort (b.begin(), b.end()); // std::sort (a.begin(), a.end()); }std::vector<B> ist sortierbar, std::vector<A> ist es nicht, es hängt also gar nicht vom Container ab, sondern von T. Wenn t.operator< oder operator<(T,T) existiert, dann ist auch jeder Sequenzcontainer sortierbar.
-
Konrad Rudolph schrieb:
camper schrieb:
Konrad Rudolph schrieb:
'size' muss eine Memberfunktion sein.
- Warum? Der gleiche Fehlschluss stört mich auch bei Sutters ansonsten excellenter Analyse von basic_string. Was hat die Wahl des Interfaces mit der Implementierbarkeit der Funktionalität zu tun? Oder bei Sutter - warum sollte es den Anwender interessieren, welcher der verschiedenen Overloads nun der Allgemeinste ist (der kriegt die Membersyntax)
So argumentiere ich ja gar nicht. Andererseits: Wie würdest Du es machen? 'size' als Algorithmus implementieren? Denkbar. Aber was wären die Vorteile?
Mir ging es ja in meiner Argumentation darum, dass ich die Unterscheidung grundsätzlich für ziemlich willkürlich halte. Eine Schnittstelle ist für mich konzeptuell das, was man mit einer Klasse (bzw. Exemplaren davon) machen kann.
Das war nicht gegen deine Argumentation gerichtet, im Gegenteil. Ich denke wir sind hier auf gleicher Wellenlänge. Es ist nicht nur für den Nutzer lediglich ein rein syntaktischer Unterschied (wenn wir jetzt mal vom Name-lookup absehen), sondern auch aus Sicht der Implementation. Wenn ein Interface nur eine Fassade um etwas herum ist, dann muss sie ohnehin alles delegieren.
-
~john schrieb:
linu(x)bie schrieb:
Wenn ein Container/Collection jedoch das Interface (z.B.) "Sortable" bereitstellt, dann ist das eine Eigenschaft!
Eine Sequenz mit Objekten vom Typ T ist genau dann sortiertbar, wenn T eine Ordnungsrelation (hier operator<) besitzt. Daher ist die Eigenschaft sortierbar keine des Sequenzcontainers, sondern sie ist eine der Elemente.
Nicht jeder Container ist ein Sequenzcontainer. Versuch mal ein Hashtable zu sortieren

-
Man programmiert mit C++ genauso OO wie mit Java. Ob du OOPst ist eine Designfrage und hat nichts mit der Sprache zu tun. Eine static methode in einer Utility Klasse ist genauso wenig OO wie eine Funktion in einem Namespace. Schade ist halt, dass man in der STL alles in einen Namespace gestopft hat.
-
1600x1200 schrieb:
Man programmiert mit C++ genauso OO wie mit Java. Ob du OOPst ist eine Designfrage und hat nichts mit der Sprache zu tun. Eine static methode in einer Utility Klasse ist genauso wenig OO wie eine Funktion in einem Namespace. Schade ist halt, dass man in der STL alles in einen Namespace gestopft hat.
Dem wiederspreche ich. Ja, im Kern ist OO Sprachunabhängig, aber je nach dem wie die Sprache ausgelegt ist, ist es manchmal nicht sinnvoll sich 100% auf klassisches OO zu beschränken (z.B. machen Operatoren außerhalb der Klasse in C++ sehr viel Sinn, Hilfsfunktionen habe ich auch lieber als notgedrungen alles in Klassen zu stopfen - wenn gleich ich ansonsten ein OO-Beführworter bin). Und C++ ist nunmal keine OO Sprache sondern wie schon angesprochen eine Multiparadigmensprache.
Wenn ich C++ Programmiere ist das etwa 70% OO, der Rest baut auf generische Programmierung 20% ob mit oder ohne Klassen und ungebundenen Hilfsfunktionen 10% auf. Wobei ich immer mit Namensräumen arbeite so das man, entsprechende UML-Tools vorausgesetzt, diese in UML mit Hilfsklassen simulieren kann.
Wenn ich andere Sprachen verwende (z.B. Java/C#) sieht das natürlich wieder anders aus.
cu André
-
"Modernes OOP"? Ich glaube deine (linuxbie) Vorstellung von OOP entspricht eher dem 90er-OOP denken. Große Vererbungshierachien sind nichts schönes und flexibles. Deshalb benutzt man heutzutage lieber späte Typbindungen (nicht nur in C++. Schau dir zB Python oder Ruby an). Wenn man Eigenschaften über Vererbungshierachien ausdrückt, muss man einfach zu viele Dinge apriori wissen. Die Anforderungen ändern sich schnell und plötzlich ergeben sich andere Usecases etc. Sollte man jedes mal den Code umwerfen und neue Eigenschaften in die Vererbungshierachie einfügen? Sicher nicht. Noch schlimmer ist es, wenn man zwei unterschiedliche Bibliotheken kombinieren muss, wie Klassen mit der gleichen Eigenschaft haben und diese auch durch Vererbung ausdrücken. Aber beide Bibliotheken haben eigene Interfaces/Basisklassen für diese Eigenschaft, so dass man eben doch nicht interoperabel sein kann.
Deshalb nimmt man lieber späte Typbindung. So wie es zB die STL macht.
Freie Funktionen sind auch nichts böses. Siehe http://www.ddj.com/cpp/184401197
@Konrad Rudolph
Das was du vorschlägst heißt Concepts und wird Bestandteil von C++0x. (Implementierung gibt es zB schon als ConceptGCC).
-
rüdiger schrieb:
@Konrad Rudolph
Das was du vorschlägst heißt Concepts und wird Bestandteil von C++0x.Habe ich doch schon geschrieben. Aber das was ich meine, geht noch ein Stück weiter als Concepts.