Defaultwerte überschreiben
-
Nexus schrieb:
Erzähl mir nicht, dass du jede einzelne Funktion nachlesen gehst.
Ich kann dir definitiv erzählen, dass ich vor jeder Verwendung einer Klasse, diese zuerst mal in der Dokumentation anschaue. Was bietet sie an, wie ist sie aufgebaut, wie verwendet man sie.
Nexus schrieb:
Wenn man bei einer Defaultparameter-Funktion die voll qualifizierte Version bereits kennt und sieht, dass es einen Defaultparameter gibt, hat man keinen Aufwand. Ist zwar nur ein kleiner Vorteil, aber ist einer.
Halte ich nicht mal für einen Vorteil, sondern für eine zusätzliche Gefahrenquelle, auf welche der Benutzer der Bibliothek achten muss.
Nexus schrieb:
Dravere schrieb:
Defaultparameter sind versteckt, daher sind sie gefährlich, während wenn du die Parameter selber übergibst, es kein Problem darstellt.
Überladung hat diesbezüglich das genau gleiche Problem. Wenn du kein explizites Argument übergibst, wird ein verstecktes übergeben. Dessen Implementierung kann sich genauso ändern. Klar kann man mit Überladung eine unabhängige Funktionalität verbinden, aber diese Distanz besteht bei Defaultparametern nicht.
Ich möchte nochmals darauf hinweisen, worum es mir genau ging. Es geht darum, bei wem schlussendlich die Verwantwortung liegt. Bei Defaultparameter liegt die Verantwortung beim Programmierer, welcher die Bibliothek verwendet. Er muss darauf aufpassen, dass etwas korrekt läuft, was vor ihm versteckt wird! Wenn es eine Überladung ist, dann liegt die Verantwortung klar beim Autoren der Bibliothek.
Nexus schrieb:
Du hast die Angewohnheit, vorschnell Schreibfaulheit in ein Argument hineinzuinterpretieren.
Hab ich das wirklich? Ist mir noch nicht aufgefallen

Nexus schrieb:
Doch lies meinen Satz nochmals, da steht lediglich "[ich] muss aber mehr Methoden implementieren, überblicken und konsistent halten".
Mehr Methoden implementieren -> du willst nicht mehr schreiben ... stimmt doch?
Mehr Methoden überblicken und konsistent halten -> Darauf bin ich danach eingegangen, wo ich gleich auch nochmals eingehen werde
Nexus schrieb:
Ich sagte ja auch nur "mehr" und nicht "sehr viel mehr".
Könntest du dieses "mehr" mal ein wenig genauer spezifizieren, denn ich halte dies für sehr wichtig bei deiner Argumentation. Wenn du davon sprichst, dass du mehr Methoden überblicken und konsistent halten musst, und dies als Nachteil angibst, dann klingt das für mich, als wenn du 30 zusätzliche Methoden nun hättest. Nach meiner Erfahrung sind es aber vielleicht 1 bis 5. Und bei so wenig zusätzlichen Methoden, kann ich dein Argument einfach nicht gelten lassen. Das ist so mikrig wenig, dass es gar nicht ins Gewicht fällt.
Nexus schrieb:
Viel wichtiger ist jedoch, dass der genannte Mehraufwand mir oft keinen Vorteil bringt.
Vieles bringt einem nicht immer sofort oder überhaupt jemals einen Vorteil. Es geht hier allerdings gerade nicht um einen sofortigen Vorteil. Der Vorteil kann sich erst viele Jahre später äussern. Wir befolgen doch ständig Richtlinien, welche aktuell eher mühsam sind, später aber sehr nützlich sein können. Wer dokumentiert zum Beispiel seinen Quellcode schon gerne? Also ich definitiv nicht und es hat auch gar keinen Vorteil für mich aktuell. Wenn ich später die Sache dann nochmals lese, bin ich dann womöglich froh drum.
Nexus schrieb:
Ja, aber du verunmöglichst mit privaten virtuellen Funktionen auch Aufrufe von Basisklassenmethoden in einer überschriebenen Methode. Und sag jetzt nicht, das sei auch schlechtes Design.
Wenn wir ganz streng sind und uns in diesem NVI Idiom bewegen, doch, dann wäre es schlechtes Design. Denn du musst dann gar nie die Basisfunktionalität aufrufen, denn die Basisklasse hat diese bereits ausgeführt und erst danach an die virtuelle Funktion weitergeleitet. Am Ende führt die Basisklasse dann womöglich wieder selber Code aus. Du musst somit gar nie die Basismethode aufrufen, denn diese hat gar keinen Code.
Allerdings muss man Richtlinien auch nicht als Dogmen auffassen. Man kann auch mal die Funktion protected machen

Grüssli
-
Dravere schrieb:
Ich kann dir definitiv erzählen, dass ich vor jeder Verwendung einer Klasse, diese zuerst mal in der Dokumentation anschaue. Was bietet sie an, wie ist sie aufgebaut, wie verwendet man sie.
Sich Überblick verschaffen ist ja auch ganz okay. Aber sich jede offensichtliche Funktionsdokumentation genau durchzulesen, nur um zu merken, dass sie gleich viel wie der Funktionsname aussagt... Naja.

Dravere schrieb:
Ich möchte nochmals darauf hinweisen, worum es mir genau ging. Es geht darum, bei wem schlussendlich die Verwantwortung liegt. Bei Defaultparameter liegt die Verantwortung beim Programmierer, welcher die Bibliothek verwendet. Er muss darauf aufpassen, dass etwas korrekt läuft, was vor ihm versteckt wird! Wenn es eine Überladung ist, dann liegt die Verantwortung klar beim Autoren der Bibliothek.
Beim Defaultparameter wird insofern nichts versteckt, als dem Aufrufer klar ist, wie ein Aufruf aussieht – nämlich genau gleich, wie wenn er den Wert hinter dem = selbst übergeben würde. Bei der Überladung hingegen muss man sich vergewissern. Besonders die Bibliothek einige Funktionen enthält, bei denen man ohne das letzte Argument ein Default-Verhalten kriegt, aber dann plötzlich eine Ausnahme vorkommt, ist sowas heikel. Ich will damit nur sagen, dass Überladung auch nicht nur Vorteile hat. Aber natürlich hat sie welche.
Dravere schrieb:
Mehr Methoden implementieren -> du willst nicht mehr schreiben ... stimmt doch?
Mit "implementieren" meinte ich weniger den Aufwand (denn dieser ist hier wirklich recht klein) als das anschliessende Vorhandensein der Methoden. Sorry, war vielleicht wirklich etwas missverständlich.
Dravere schrieb:
Könntest du dieses "mehr" mal ein wenig genauer spezifizieren, denn ich halte dies für sehr wichtig bei deiner Argumentation. [...] Nach meiner Erfahrung sind es aber vielleicht 1 bis 5.
Ich habe auch etwa in dieser Grössenordnung gedacht, wobei ich mich nicht auf 5 als obere Grenze beschränken würde. Aber fünf unnötige Methoden sind zuviele (ich spreche immer noch vom Fall, dass ich keinen langfristigen Vorteil durch die Indirektion sehe; nicht dass du mich falsch verstehst).
Dravere schrieb:
Vieles bringt einem nicht immer sofort oder überhaupt jemals einen Vorteil. Es geht hier allerdings gerade nicht um einen sofortigen Vorteil. Der Vorteil kann sich erst viele Jahre später äussern.
Voraussicht ist gut, aber man kann nicht immer so denken. Jede Implementierung und jedes Design läuft Gefahr, mit der Zeit Probleme zu verursachen. NVI einzusetzen kann sich später auch als Fehler erweisen, z.B. wenn man plötzlich Basisklassenmethoden aufrufen möchte. Oder wenn die ganze Zeit eine unnötige Komplexität vorhanden war, deren Vorteil man nicht ausspielen konnte oder die sich sogar negativ auswirkte. Um andere Beispiele zu nennen: Du kannst nicht jede Klasse und Funktion als Template schreiben, um später eine allfällige Generizität zu ermöglichen. Oder jede Klasse in drei kleinere aufteilen, weil es möglicherweise einmal erwünscht sein kann, dass nur ein Teil der Funktionalität benötigt wird. Oder für alle verwendeten Typen zuerst mit
typedefAliase einführen, um so abstrakt und frei wie möglich zu bleiben. Stichwort Overenginnering.Es müssen Designentscheidungen getroffen werden, die sich später vielleicht als falsch und ungeeignet herausstellen. Ich versuche, NVI wo sinnvoll einzusetzen, aber ebenso, es an unnötigen Stellen zu vermeiden.
Du hast es bestimmt auch schon erlebt, dass du etwas recht Komplexes implementiert hast – vielleicht mit tiefer Metaprogrammierung und am Rande des in C++ Möglichen – und danach stolz auf das Resultat warst. Und später kommt die Einsicht, dass man es mit dem naheliegenden, einfachen Ansatz mindestens gleich gut hätte vollbringen können. Lustig finde ich dieses Beispiel (Folie 15-18, übrigens sehr interessante Präsentation!).
Dravere schrieb:
Wir befolgen doch ständig Richtlinien, welche aktuell eher mühsam sind, später aber sehr nützlich sein können. Wer dokumentiert zum Beispiel seinen Quellcode schon gerne?
Je nach Code, an dem ich gerade programmiere, kann es eine gute Abwechslung sein.

Dravere schrieb:
Wenn wir ganz streng sind und uns in diesem NVI Idiom bewegen, doch, dann wäre es schlechtes Design. Denn du musst dann gar nie die Basisfunktionalität aufrufen, denn die Basisklasse hat diese bereits ausgeführt und erst danach an die virtuelle Funktion weitergeleitet.
Mit einer Zwischenklasse ist es aber nicht möglich. Die oberste Basisklasse kann nur die Funktion der am meisten abgeleiteten Klasse aufrufen, und umgekehrt.
-
Nexus schrieb:
Sich Überblick verschaffen ist ja auch ganz okay. Aber sich jede offensichtliche Funktionsdokumentation genau durchzulesen, nur um zu merken, dass sie gleich viel wie der Funktionsname aussagt... Naja.

Ich habe schon erlebt, dass zum Beispiel ein
SetPositionnicht nur die Position gesetzt hat, sondern diese gleich noch übers Netzwerk geschickt hat, obwohl es eigentlich eine weitere Methode gab, welche für das Synchronisieren zuständig war. Stand wunderbar in der Dokumentation, empfand ich allerdings als völlig unlogisch und wäre ich nie darauf gekommen, hätte ich die Dokumentation nicht gelesen. Die grössten Programmierfehler entstehen genau daraus: Der Programmierer nimmt an, dass es so geht, wie er denkt. Auch bei den kleinsten und unscheinbarsten Methoden kann dies plötzlich nicht der Fall sein. Und das Lesen von Getter und Setter Dokumentationen ist wirklich nichts sehr aufwendiges, vielleicht ein wenig öde (:D), aber dafür hat man danach Gewissheit ... sofern die Dokumentation korrekt ist
Nexus schrieb:
Beim Defaultparameter wird insofern nichts versteckt, als dem Aufrufer klar ist, wie ein Aufruf aussieht – nämlich genau gleich, wie wenn er den Wert hinter dem = selbst übergeben würde.
Er sieht diesen Wert aber nicht, weil er sich irgendwo in einem Header befindet, in der tief verschachtelten Hierarchie. Es kommt von der Sichtbarkeit fast genau gleich wie bei einer überladenen Methode, welche dies völlig versteckt. Ich stimme dir absolut zu, dass die Überladung nicht nur Vorteile birgt, aber sie ist meiner Meinung nach vorteilhafter als Defaultparameter, vor allem eben wegen der Zuständigkeit.
Nexus schrieb:
Aber fünf unnötige Methoden sind zuviele (ich spreche immer noch vom Fall, dass ich keinen langfristigen Vorteil durch die Indirektion sehe; nicht dass du mich falsch verstehst).
Das in den Klammern verstehe ich jetzt gar nicht. Oder ich habe eine Vermutung, ich komme darauf aber weiter unten zurück.
Nexus schrieb:
... [voraussicht] ...
Versteh mich bitte nicht falsch, ich will das nicht als Dogma einführen und alles ausschliesslich so verwenden. Ich stimme dir zu, dass es sicher Ausnahmen gibt, wo man es besser nicht tun sollte. Ich finde es nur ein sehr interessantes Design, welches man ruhig öfters einsetzen sollte. (übrigens, wir entfernen uns langsam immer wie mehr vom Thread-Thema :p)
Nexus schrieb:
Mit einer Zwischenklasse ist es aber nicht möglich. Die oberste Basisklasse kann nur die Funktion der am meisten abgeleiteten Klasse aufrufen, und umgekehrt.
So, hier kehre ich nochmals auf deine Klammeraussage von oben zurück. Hier scheint es mir, dass du das Idiom nicht ganz verstanden hast, denn es ist eigentlich gar nie nötig, auf eine virtuelle Methode einer Basisklasse (egal, ob es nur eine Zwischenklasse ist) zuzugreifen. Die Idee ist ja, dass man die virtuelle Funktion auf das wesentliche beschränkt, auf das was wirklich virtuell sein muss. Vielleicht an etwas pseudo C++ Code erklärt, wie dies aussehen kann:
class Base { private: virtual void execute_strategy(int param) = 0; virtual void preload_strategy(strategy const&) = 0; private: void init_execution_context() { // ... } void save_execution_context() { // ... } protected: void do_basic_things() { // ... // Wird womöglich von init_execution_context verwendet // oder wie auch immer, soll aber auch für Klassen in // der Vererbungshierarchie zur Verfügung gestellt // werden. } public: void use_strategy() { init_execution_context(); execute_stragety(2423); save_execution_context(); } void set_strategy(strategy const& strategy) { // Können erneut was zuvor ausführen. // In dieser Version aber noch nicht nötig. preload_strategy(strategy); // In dieser Version auch am Ende noch nichts nötig. } };Ist das so klarer? Es ist halt eine sehr strikte Aufgabenauftrennung, welche definitiv nicht immer sinnvoll ist wie so oft bei Richtlinien und Empfehlungen.
Grüssli
-
Dravere schrieb:
Ich habe schon erlebt, dass zum Beispiel ein
SetPositionnicht nur die Position gesetzt hat, sondern diese gleich noch übers Netzwerk geschickt hat, obwohl es eigentlich eine weitere Methode gab, welche für das Synchronisieren zuständig war. Stand wunderbar in der Dokumentation, empfand ich allerdings als völlig unlogisch und wäre ich nie darauf gekommen, hätte ich die Dokumentation nicht gelesen. Die grössten Programmierfehler entstehen genau daraus: Der Programmierer nimmt an, dass es so geht, wie er denkt.Hier sehe ich den Fehler aber mehr beim Bibliotheksentwickler. Bei anti-intuitiven Funktionen darf man sich nicht über Fehlbenutzung wundern. Für irgendwas gibts ja treffende Funktionsnamen und Trennung der Aufgabenbereiche. Bei
a-berwartet auch niemand, dass in Wirklichkeit eine Multiplikation durchgeführt wird undaanschliessend 0 ist.
Natürlich sollte man lieber einmal zu viel als zu wenig in die Dokumentation schauen, besonders wenn man eine Bibliothek noch nicht gut kennt.
Dravere schrieb:
Er sieht diesen Wert aber nicht, weil er sich irgendwo in einem Header befindet, in der tief verschachtelten Hierarchie.
Er sieht den Wert in seiner modernen IDE oder in der Dokumentation, die er lesen sollte. :p
Aber im Ernst: Ich habe schon oft die IntelliSense-Informationen benutzt, um mich über eine Schnittstelle zu informieren. Wenn die Dokumentation gleich über der Funktion steht (z.B. bei Doxygen), ist das viel praktischer als eine externe HTML-Datei abzusuchen.
Dravere schrieb:
Nexus schrieb:
Aber fünf unnötige Methoden sind zuviele (ich spreche immer noch vom Fall, dass ich keinen langfristigen Vorteil durch die Indirektion sehe; nicht dass du mich falsch verstehst).
Das in den Klammern verstehe ich jetzt gar nicht.
Ich entscheide je nach Fall, ob ich NVI für sinnvoll halte oder nicht. In der Klammer ging es mir ums Nicht Sinnvoll. Diese hatte ich am Schluss noch kurz hinzugefügt in der Hoffnung, meine Ansicht zu verdeutlichen. Aber scheinbar habe ich das Gegenteil erreicht und noch mehr Verwirrung gestiftet.

Was ich ursprünglich damit sagen wollte: Du solltest "fünf unnötige Methoden sind zuviele" nicht so verstehen, dass ich durch NVI zusätzlich entstandene Methoden generell für unnötig halte.
Dravere schrieb:
Hier scheint es mir, dass du das Idiom nicht ganz verstanden hast, denn es ist eigentlich gar nie nötig, auf eine virtuelle Methode einer Basisklasse (egal, ob es nur eine Zwischenklasse ist) zuzugreifen.
Danke für die Erklärung. Im Normalfall ist es nicht nötig, das stimmt.
Was ich eigentlich meinte, war Folgendes:
class Fahrzeug { public: void TankeAuf() { // erhalte Möglichkeit für Zusatzbehandlung, Prüfungen etc. TankeWirklichAuf(): } private: virtual void TankeWirklichAuf() = 0; }; class Schiff : public Fahrzeug { private: virtual void TankeWirklichAuf() { FuelleHauptTanks(); FuelleNebenTanks(); } void FuelleHauptTanks(); void FuelleNebenTanks(); }; class HyperSchiff : public Schiff { private: virtual void TankeWirklichAuf() { Schiff::TankeWirklichAuf(); // <- leider nicht möglich FuelleHyperTanks(); } void FuelleHyperTanks(); };Ich denke aber, wir sehen das Ganze doch nicht so unterschiedlich.

-
Nexus schrieb:
Dravere schrieb:
Ich habe schon erlebt, dass zum Beispiel ein
SetPositionnicht nur die Position gesetzt hat, sondern diese gleich noch übers Netzwerk geschickt hat, obwohl es eigentlich eine weitere Methode gab, welche für das Synchronisieren zuständig war. Stand wunderbar in der Dokumentation, empfand ich allerdings als völlig unlogisch und wäre ich nie darauf gekommen, hätte ich die Dokumentation nicht gelesen. Die grössten Programmierfehler entstehen genau daraus: Der Programmierer nimmt an, dass es so geht, wie er denkt.Hier sehe ich den Fehler aber mehr beim Bibliotheksentwickler. Bei anti-intuitiven Funktionen darf man sich nicht über Fehlbenutzung wundern. Für irgendwas gibts ja treffende Funktionsnamen und Trennung der Aufgabenbereiche. Bei
a-berwartet auch niemand, dass in Wirklichkeit eine Multiplikation durchgeführt wird undaanschliessend 0 ist.
Naja. In gewissem sind alle beteiligten Schuld.

Weil es ja explizit in der Dokumentation steht. Ich halte das aber eher für einen Sonderfall, als die Regel. Kommt halt drauf an, auf was für ein Objekt man das aufruft. Von einem string erwarte ich nicht, dass er irgendwelche Sachen ins Netz propagiert, was ich bei einem Socket nicht ausschliessen kann.
Auf der anderen Seite hätten die Entwickler das schon trennen können. Wir (ausser Dravere) können aber die genauen Umstände gar nicht kennen.. Könnte imo schon auch Sinn machen, dass sich die Klasse da unintuitiv verhält.Klar sollte aber schon die Intuition nicht die Dokumentation ersetzen, sondern sie unterstützen.
Aber z.B habe ich, als ich vor ein paar Monaten mit Java angefangen habe nie wirklich die gesamte Doku zu den Collections gelesen, sondern einfach mehr oder weniger intuitiv benutzt. Wenn ich dann etwas doch nachgelesen habe, dann stand da eigentlich immer sowieso das, was ich angenommen habe. (Elemente einfügen, entfernen, suchen, zugreifen usw. - wüsste nicht, warum ich das lesen hätte sollen)
-
Nexus schrieb:
Hier sehe ich den Fehler aber mehr beim Bibliotheksentwickler. Bei anti-intuitiven Funktionen darf man sich nicht über Fehlbenutzung wundern.
Problem ist, dass das intuitive Empfinden oft sehr subjektiv ist.

Nexus schrieb:
Was ich eigentlich meinte, war Folgendes:
Und wieso nicht so:
class Fahrzeug { public: void TankeAuf() { // erhalte Möglichkeit für Zusatzbehandlung, Prüfungen etc. TankeWirklichAuf(): } private: virtual void TankeWirklichAuf() = 0; }; class Schiff : public Fahrzeug { private: virtual void TankeWirklichAuf() { FuelleAlleTanks(); } protected: void FuelleAlleTanks() { FuelleHauptTanks(); FuelleNebenTanks(); } void FuelleHauptTanks(); void FuelleNebenTanks(); }; class HyperSchiff : public Schiff { private: virtual void TankeWirklichAuf() { Schiff::FuelleAlleTanks(); // <- geht doch! ;) FuelleHyperTanks(); } void FuelleHyperTanks(); };Nexus schrieb:
Ich denke aber, wir sehen das Ganze doch nicht so unterschiedlich.

Glaube ich auch nicht

Grüssli
-
Dravere schrieb:
Und wieso nicht so:
Genau.

Aber eben: Streng genommen ist es ja nicht mehr privat. Ich glaube jedoch, wir sind uns einig, was das betrifft.
Danke aber, dass du dir solche Mühe für das Ändern der Einrückung gegeben hast.

Und sorry drakon, dass deine Posts in der Flut untergegangen sind.

(Keine Sorge, ich habe sie schon gelesen und stimme ihnen weitgehend zu :)).
-
Nexus schrieb:
Und sorry drakon, dass deine Posts in der Flut untergegangen sind.

(Keine Sorge, ich habe sie schon gelesen und stimme ihnen weitgehend zu :)).Schon gut. Ich wollt euch ja nicht unterbrechen.

-
Nexus schrieb:
Aber eben: Streng genommen ist es ja nicht mehr privat. Ich glaube jedoch, wir sind uns einig, was das betrifft.

Jein. Man kann es wirklich
privatemachen, wenn man möchte und muss.
Vielleicht nur eine kleine Ergänzung noch, welche mir gestern spät am Abend noch einfiel, wo der Computer aber schon aus war.
Das praktische ist, wenn die virtuelle Funktion aufgerufen wird, dann weiss man, dass es den Aufruf der letzten Überschreibung ist. Die Methode kann schliesslich nicht anders aufgerufen werden (abgesehen davon, wenn der Programmierer es selber in der eigenen Klasse macht). Du hast somit einen sehr klar definierten Zustand, welchen du ausnützen kannst, wenn du musst.Nexus schrieb:
Danke aber, dass du dir solche Mühe für das Ändern der Einrückung gegeben hast.

Ist das eigentlich dein Standardeinrückungsstil? Sollen wir über sowas auch noch diskutieren?

@drakon,
Oh, du bist ja auch noch hier
Grüssli
-
Dravere schrieb:
Ist das eigentlich dein Standardeinrückungsstil?
Ja. Ein Tab ist bei mir so gross wie vier Leerzeichen, und Zugriffsspezifizierer fangen eingerückt an.
Ich habe mir allerdings auch schon überlegt, ob ich die Einrückungsebene sparen sollte. Z.B. bei
switchist es ähnlich:switch (var) { case 1: Bla(); break; default: Bli(); break; }vs.
switch (var) { case 1: Bla(); break; default: Bli(); break; }Ich finde die erste Variante leicht übersichtlicher, und der Bildschirm ist eigentlich genügend breit. Klassen haben dann für
friends die gleiche Einrückungsstufe wie fürpublic:.Du benutzt nur zwei Leerzeichen pro Einrückungsebene?
-
Nexus schrieb:
Du benutzt nur zwei Leerzeichen pro Einrückungsebene?
Oh, das ist aber schön. Ich benutze auch gerne zwei.

-
Nexus schrieb:
Du benutzt nur zwei Leerzeichen pro Einrückungsebene?
Im Forum ja, weil es einfacher ist und grundsätzlich nicht mehr benötigt. Ein einziges Leerzeichen ist allerdings zu wenig. Ansonsten verwende ich aber oft Tab und lasse ihn durch 4 Leerzeichen ersetzen. Ich muss allerdings gestehen, dass ich schon mit dem Gedanken gespielt habe, überall auf 2 Leerzeichen runter zu gehen. Es reicht grundsätzlich völlig aus, also wieso noch mehr in die Breite gehen? Vielleicht mache ich mal einen Kompromiss und nehme 3

Grüssli
-
In machen Sprachen benutze ich nur eins. Aber drei? Auf die Idee, eine Nicht-Zweierpotenz zu nehmen, wäre ich nie gekommen.