Modernes OOP unter C++
-
Danke für die Antwort

Maxi schrieb:
Die freien Funktionen, wie du sie nennst, sind in der STL algorithmen, wie std::copy. Wie sollte man die auch in eine Klasse packen?
Naja, schau Dir hierzu zB das Collections-Framework von Java bzw auch .NET an. Das ist etwas was ich als "sauber" bezeichnen würde.
Bei der STL scheint es mir fast so, als ob das ganze auch für die "prozedurale Programmierung a la C" ausgelegt wurde. Die STL soll hier aber nicht das Thema meiner Frage sein.Maxi schrieb:
Für programmierstil schau dir doch Bibliotheken für c++ an, gibt ja genug bei bspw. sf.net
Naja, dass erscheint mir im Endeffekt wohl eine gute Vorgehensweise zu sein. Ich würde aber gerne von den alten Hasen (die vielleicht auch Erfahrung mit Java oder .NET haben) konkret wissen, was zu empfehlen ist.
Gruß
-
linu(x)bie schrieb:
ich komme aus der "Fraktion Java" und lerne nun C++. Aus Java bin ich es nun gewohnt strikt OO zu programmieren. Über die Jahre unter Java habe ich mir nun einen sauberen OO-Stiel angewöhnt. Leider scheint unter C++ OOP und tiefe Klassenhierarchien kein so tragendes Thema zu sein.
OOP ja - aber nicht nur OOP - tiefe Klassenhierarchien eher nicht, das ist richtig.
linu(x)bie schrieb:
Stattdessen wird eher auf Templates gesetzt (siehe STL).
genau - der Unterschied zu Java und C# ist, dass in C++ vieles zur Compile-Zeit 'erledigt' werden kann. Wohlgemerkt 'kann' nicht muss. C++ wird nicht umsonst als 'Multi Paradigmen Sprache' bezeichnet. Ich meine, man kann in C++ auch so programmieren wie in Java. Nur es würden einen dann die umfangreichen System-Bibliotheken fehlen.
Das macht C++ nicht gerade einfach, schon gar nicht für den, der Bibliotheken programmiert.linu(x)bie schrieb:
Auch die "ungebundenen Funktionen" bereiten mir Kopfschmerzen

was verstehst Du unter 'ungebundenen Funktionen'?
Falls Du damit z.B. die Algorithmen der STL meinst - warum bereiten die Dir Kopfschmerzen?linu(x)bie schrieb:
Hierzu wollte ich Euch nach (fortgeschrittenen) Quellen zum Design von Programmierbibliotheken, nach einem sauberen und (von Euch) empfohlenen Still fragen. Willkommen sind Links, Buchempfehlungen oder Beispielcode.
Das Design von Programmierbibliotheken ist mit das schwerste, was es beim Programmieren gibt. Als Klassiker fällt mir dazu nur 'Modern C++ Design' von Andrei Alexandrescu ein. Aber es ist in diesem Sinne nicht vollständig - es setzt voraus, dass Du das schon beherrschst, das Buch zeigt dann, was man in C++ darüber hinaus noch machen kann.
Als Beispiel für 'best practise' kann ich uneingeschränkt die boost-Bibliotheken empfehlen. Aber frag hier im Forum ruhig zurück, falls Dir da was 'komisch' vorkommt.
Gruß
Werner
-
Hi linu(x)bie,
als kleine Ergänzung zu Werner.
linu(x)bie schrieb:
...Aus Java bin ich es nun gewohnt strikt OO zu programmieren....
Vielleicht solltest Du Dir angewöhnen, jeweils mit dem (zur Problemstellung) passendsten Paradigma zu programmieren.

Gruß,
Simon2.
-
Du solltest dich von dem Gedanken lösen dass OOP so zu sein hat wie in Java. Da sind einige Sachen richtig gemacht worden und einige falsch, und nur weil etwas nicht ist wie in Java ist es noch lange nicht unsauber. Guck dir ein Reihe von objektorientierten Sprachen an, Smalltalk, Common Lisp, Python, Eiffel usw., dann kannst du dir darüber vielleicht eine Meinung bilden.
In C++ war es zu früherer Zeit auch "in", mit tiefen Vererbungshierarchien zu arbeiten. Aber gerade, weil man von der Sprache her nicht so darauf festgelegt ist, hatten C++ler Gelegenheit, sich viele Gedanken über Design zu machen. Das Ergebnis ist die STL und später Boost. Das sind keine prozeduralen Altlasten.
-
Das Problem ist schon die Definition von "was ist OOP", bevor man sich darüber unterhält, wie weit C++ OOP ist - siehe auch hier
Dazu kommt, dass viele freie Funktionen zu bestimmten Klassen gehören und damit nicht mehr so frei wie sie eigentlihc ausehen - mehr dazu hier.Grundsätzlich sollte man evtl. deine Aussage relativieren:
Du bist es gewohnt mit den Java-Mitteln strikten Java-OO-Stil zu programmieren und hast dir einen sauberen Java-OO-Stil angewöhnt.Es ist nunmal so, dass C++ und Java verschiedene Sprachen sind, die verschiedene Aspekte der OOP verschieden behandeln und wohl auch auf verschiedenen Definitionen von dem aufbauen, was eigentlich OO sein soll. Dementsprechend kannst du nicht erwarten, dass du C++ im Java-OO-Stil programmieren kannst und andersrum. Gäbe es nur eine wirkliche Definition von Objektorientierung und nur eine einzige richtige Möglichkeit, diese umzusetzen, dann gäbe es auch nur eine objektorientierte Programmiersprache - vermutlich Eiffel

-
linu(x)bie schrieb:
Naja, schau Dir hierzu zB das Collections-Framework von Java bzw auch .NET an. Das ist etwas was ich als "sauber" bezeichnen würde.
Oberflächlich betrachtet vielleicht. Dafür sind diese Collection-Frameworks weitaus weniger mächtig als das was die STL bietet. Unter .NET fällt mir z.B. immer wieder auf dass Kopieren nur zwischen Collections und Arrays (nicht zwischen bel. Collections) möglich ist, dass die ForEach-Methode nur von wenigen Collections angeboten wird, uvm.. C++ Container und Algorithmen kann man grundsätzlich beliebig kombinieren.
-
linu(x)bie schrieb:
...
Wie schon erwähnt liegen zwischen Java und C++ mehr als nur kleine Unterschiede. Nach deinen Anmerkungen zu Urteilen solltest du dir vielleicht mal das Buch "Effektiv C++ Programmieren" (Scott Meyer) anschauen. Ich glaube der erste Tip geht schon darauf ein das man in C++ eben beachten sollte das es eine Multiparadigmensprache ist mit teilweise abweichenden Regeln. Auch gut ist Herb Sutter mit seinen Erklärungen (Google mal nach "Guru of the Week") auch wenn diese vermutlich für dich als C++ Einsteiger teilweise nicht ganz verständlich sein werden...
Ich sage mal die zwei großen Themen in C++ sind auf der einen Seite OOP auf der anderen Seite die generische Programmierung mittels Templates. Dabei sollte man sich aber niemals zu Verbissen an ein Paradigma klammern. Man wählt das jeweils passende und Zweckmäßige aus. Es hat z.B. Gründe der Effizienz und Wiederverwertbarkeit das auch ungebundene Funktionen ihre Daseinsberechtigung haben.
Ganz davon abgesehen: Tiefe Klassenhierachien sind auch kein Zeichen von sauberen OOP. 3 oder vielleicht 4 Ebenen mag noch überschaubar sein, danach leidet der Überblick aber sehr stark. Zudem ist Vererbung ein zweischneidiges Schwert und häufig ist Komposition die bessere Wahl. Es gibt auch Anwendungsfälle für tiefere Vererbungshierachien, aber sauber ist das nicht zwangsweise.
OO ist ein Ansatz der Programmierung, aber man hat schon festgestellt das OO für sich genommen auch noch nicht der Heilige Gral ist. In C++ kann man sich das herauspicken was je nach Ansatz das Beste ist, was sowohl ein Vorteil wie ein Nachteil (Gefahr sich falsch zu entscheiden) sein kann.
cu André
-
asc schrieb:
...Es hat z.B. Gründe der Effizienz und Wiederverwertbarkeit das auch ungebundene Funktionen ihre Daseinsberechtigung haben....
... oder auch das bessere Design. Ein Sortieralgorithmus braucht z.B. eine umgebende Klasse ebensowenig wie einen GUI-Button.
Gruß,
Simon2.
-
Simon2 schrieb:
asc schrieb:
...Es hat z.B. Gründe der Effizienz und Wiederverwertbarkeit das auch ungebundene Funktionen ihre Daseinsberechtigung haben....
... oder auch das bessere Design. Ein Sortieralgorithmus braucht z.B. eine umgebende Klasse ebensowenig wie einen GUI-Button.
Na ja. Mit einer hinreichend aufwendigen Klassenhierarchie könnte man das durchaus sinnvoll implementieren. 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. Das wäre dann konzeptuell gleichmächtig wie die C++-Implenentierung (vorausgesetzt, es gäbe auch die Möglichkeit, nur einen Teilbereich einer Menge zu extrahieren, was ja durchaus denkbar wäre).
-
Konrad Rudolph schrieb:
Na ja. Mit einer hinreichend aufwendigen Klassenhierarchie könnte man das durchaus sinnvoll implementieren...
Das wiederspricht aber den Designentwurf von C++ (IMHO auch zurecht).
-
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.Was passiert, wenn man anfängt, alle Algos in die Klasse zu quetschen (egal ob via Vererbung oder Aggregation), kann man an std::string sehen - und mir gefällt das nicht.
Gruß,
Simon2.
-
asc schrieb:
Konrad Rudolph schrieb:
Na ja. Mit einer hinreichend aufwendigen Klassenhierarchie könnte man das durchaus sinnvoll implementieren...
Das wiederspricht aber den Designentwurf von C++ (IMHO auch zurecht).
asc, ich wollte nicht darauf hinaus, dass man es in C++ anders machen sollte oder dass es zwangsläufig besser wäre. Ich wollte nur darauf hinweisen, dass es sich genauso implementieren lässt. Und was wichtiger ist: in einer (imaginären) geeigneten Sprache wäre es genauso effizient und flexibel. Letztendlich ist das nur Syntax, die Semantik müsste man nicht antasten.
(Mich interessiert das ganze, weil ich immer noch daran arbeite, eine solche Sprache zu designen.)
-
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 ?Auf mehrere Art und Weise (aber ich möchte nochmal darauf hinweisen: Ich meinte nicht, dass es in C++ schlecht gelöst ist). Besonders dadurch, dass man mit expliziten Type-Constraints arbeiten kann, statt wie in C++ (noch) mit impliziten. Ich denke da an ein Typsystem wie in Haskell. Nun ist es in Haskell so, dass es lediglich freie Funktionen gibt, aber das ist wie gesagt nur Syntax. Ob man nun 'sort myrange' oder 'myrange.sort' schreibt, ist vollkommen irrelevant.
Übrigens wäre die Kopplung zwischen Container und Algorithmus a priori nicht stärker als in C++. Ich spiele eigentlich nur auf das an, was C++0x mit seinen Concepts auch können wird. Damit wir nicht aneinander vorbeireden, ich stelle mir quasi folgendes vor:
trait Range[T] is Sortable # … end class Vector[T] # … class List[T] # … instance Vector of Range # (Standard-Implementierung übernehmen) instance List of Range function Sort(ord as StrictWeakOrdering) # Implementiert effiziente Suche auf verketteten Listen end endWichtig ist hierbei, dass 'Range' nichts anderes als ein Wrapper um ein Paar von Iteratoren ist und ein halboffenes Intervall darstellt. Damit ist dieser Code gleichmächtig wie sein Äquivalent in C++.
Ein weiterer Vorteil dieser Schreibweise ist Vereinheitlichung; man kann dadurch erreichen, dass es nur noch einen Typ von Funktionsaufrufen gibt (wie auch immer die Syntax dafür aussieht). Dass man in den meisten Sprachen syntaktisch zwischen Methoden und freien Funktionen unterscheidet finde ich nicht prickelnd, weil dieser Unterschied semantisch nicht getragen wird.
Ein großer Vorteil einer solchen Hierarchie ist übrigens, dass die Notwendigkeit des Argument-abhängigen Lookups (Koenig-Lookup) entfällt, ohne an Erweiterbarkeit oder Modularität einzubüßen: Jeder Funktionsaufruf fällt in einen Kontext, der allein durch sein erstes Argument den Definitionsort bestimmt und höchstens in der Vererbungshierarchie hochwandern muss.
Ich habe unter http://madrat.net/caliph/ mal etwas sehr unvollständiges und teilweise auch veraltetes darüber veröffentlicht. Evtl. ist es ja trotzdem lesenswert (aber ich muss es unbedingt mal aktualisieren).
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.
Ja, aber wo zieht man die Grenze? 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.
Was passiert, wenn man anfängt, alle Algos in die Klasse zu quetschen (egal ob via Vererbung oder Aggregation), kann man an std::string sehen - und mir gefällt das nicht.
Was genau gefällt Dir daran nicht?
-
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).