To point or not to point -- Designfrage
-
Simon2 schrieb:
P.S.: Kann mir jemand erklären, warum es
std::vector::operator[] const;nur eineconst_referencezurückgibt? Sehe ich irgendwie nicht ein...Weil std::vector::operator[] const sonst gar nicht const wär.
std::vector::operator[] gibt auch reference zurück.
-
brotbernd schrieb:
Simon2 schrieb:
P.S.: Kann mir jemand erklären, warum es
std::vector::operator[] const;nur eineconst_referencezurückgibt? Sehe ich irgendwie nicht ein...Weil std::vector::operator[] const sonst gar nicht const wär....
Erste Reaktion:
Wieso nicht?
Für mich ändert ein Container nicht automatisch seinen Zustand, wenn sich der Zustand eines seiner Element ändert...Zweite (nach ein wenig Nachdenken):
Was wäre, wenn "operator[]() const" eine "nonconst_reference" zurückgäbe:struct item { int i; }; void f(vector<item> const& v) { ++(v[0].i); // wäre ja (Arbeitshypothese) erlaubt } int main() { vector<item> v1, v2; v1.push_back(item()); v2 = v1; f(v1); if(v1 == v2) cout << "dies würde ich erwarten"; else cout << "dies würde ich (bei bisheriger Definition des operator==()) erhalten"; ...OK - ist wenigstens konsistent umgesetzt und je länger ich drüber nachdenke, desto schwerer fällt mir, eine konsistente Umsetzung meines "Containerverständnisses" zu entwerfen.
Ist wohl schon richtig so.
Danke,
Simon2.
-
Simon2 schrieb:
Für mich ist ein Zeiger immer ein Hinweis darauf, dass auch 0 übergeben werden und/oder der Verweis "verschoben" werden darf
Genau deshalb frage ich ja.
Mich interessiert, wie andere das sehen, also welches Verhalten angenommen wird, wenn im Parameter ein Zeiger oder eine Referenz auftaucht.Nehmen wir Folgendes an:
std::list<const Item*> _cont; void AddItem( const Item& item ) { _cont.push_back( &item ); }Ich finde das zeigt, dass so was hier fast schon erwünscht ist:
AddItem( Item( ... ) );Daher einfach die Verständnisfrage.
@volkard:
Genau darüber dachte ich auch nach, also über das Kenntlichmachen, dass da mit Referenzen intern gespielt wird. Ansonsten würde mir nur einfallen, eine passende Zeile mit in die Dokumentation zu quetschen.
-
StefanBo schrieb:
Genau darüber dachte ich auch nach, also über das Kenntlichmachen, dass da mit Referenzen intern gespielt wird. Ansonsten würde mir nur einfallen, eine passende Zeile mit in die Dokumentation zu quetschen.
Definitiv die passende Zeile in die Doku. Bei einer nonconst-Referenz kann man nicht davon ausgehen, dass sie nicht im Objekt gespeichert wird, und ein Zeiger ist noch lange kein Hinweis darauf, dass eine Referenz auf das Argument im Objekt gespeichert wird. Da gibts keine allgemeingültigen Coding-Styles.
Dass eine Referenz nicht null ist, ist in der Sprache verankert, damit kann man also sicherstellen dass auch immer ein Objekt übergeben wird. Dangling references mal ausgenommen.
-
pumuckl schrieb:
Dass eine Referenz nicht null ist, ist in der Sprache verankert, damit kann man also sicherstellen dass auch immer ein Objekt übergeben wird.
Aber diese Regel hat evtl keinen Nutzwert, wie ich feststellen darf, weil ich sie konsequent nicht befolge. Es ist nämlich auch so klar, wann 0 übergeben werden darf und wann nicht.
Außerdem habe ich die zweite Regel selber neulich (14.10.09) angewandt, wie ich jetzt verblüfft feststellen muß.
-
volkard schrieb:
pumuckl schrieb:
Dass eine Referenz nicht null ist, ist in der Sprache verankert, damit kann man also sicherstellen dass auch immer ein Objekt übergeben wird.
Aber diese Regel hat evtl keinen Nutzwert, wie ich feststellen darf, weil ich sie konsequent nicht befolge. Es ist nämlich auch so klar, wann 0 übergeben werden darf und wann nicht.
boost.ptr_container ist da aber ein gutes Gegenbeispiel. Ohne Doku impliziert das Interface, dass NULL übergeben werden darf. Darf es aber so erstmal nicht. Erst, wenn man das Template-Argument entsprechend Vorbereitet ist es möglich. Aber "so klar" ist das nicht.
Wie stellst Du es denn klar, wenn ich Fragen darf?
-
Tachyon schrieb:
Wie stellst Du es denn klar, wenn ich Fragen darf?
Es bedarf keiner gesonderten Klarstellung. Ich habe gar kein Verlangen, 0 zu übergeben.
-
StefanBo schrieb:
Simon2 schrieb:
Für mich ist ein Zeiger immer ein Hinweis darauf, dass auch 0 übergeben werden und/oder der Verweis "verschoben" werden darf
Genau deshalb frage ich ja.
Mich interessiert, wie andere das sehen, also welches Verhalten angenommen wird, wenn im Parameter ein Zeiger oder eine Referenz auftaucht.Nehmen wir Folgendes an:
std::list<const Item*> _cont; void AddItem( const Item& item ) { _cont.push_back( &item ); }Ich finde das zeigt, dass so was hier fast schon erwünscht ist:
AddItem( Item( ... ) );Daher einfach die Verständnisfrage.
Ich finde die Regel, immer Referenzen zu übergeben, außer man benötigt Pointer-Eigenschaften, nützlich und wende sie möglichst immer an.
Nach meinem Verständnis ist allerdings die Möglichkeit, bei Übergabe von Pointern ohne Weiteres einen Verweis statt eine Kopie zu übernehmen, auch eine Pointer-Eigenschaft. Aus diesem Grunde würde ich nie AddItem() wie im Beispiel implementieren, sondern immer mit einem Pointer als Parameter. Dass kein Nullpointer übergeben werden darf, würde ich in diesem Fall dokumentieren und vielleicht noch die Funktion in TakeItem() (oder so) umbenennen.
Stefan.
-
Ich halt mich an die Regel: "wenn die Funktion die Daten veraendert, wird ein Pointer uebergeben".
Die Logik dahinter ist, dass beim Verwenden der Funktionen sofort sehe, was die Funktion macht:
do_something(&foo); // foo kann womoeglich veraendert werden do_something(foo); // foo wird nicht veraendertWenn kein Pointer uebergeben wird gehe ich davon aus dass entweder eine Kopie oder eine const Referenz uebergeben wird. Bei AddItem wuerde ich einen Pointer uebergeben. Ein Aufruf wie "addItem(foo)" schaut fuer mich so aus, als wuerde eine KOPIE von foo uebergeben, in Wirklichkeit ist dem ja aber nicht so.
Das "ich kann auch null uebergeben"-Argument find ich nicht so wichtig. In der Regel kann ich vom Funktionsnamen ableiten, ob null uebergeben werden kann oder nicht. Und in nicht-performance-kritischen Funktionen kann ich die Parameter auch von Hand auf null testen und reagieren (einfach ignorieren, oder ein assert() oder eine Exception... je nachdem was mehr Sinn macht).
-
Blue-Tiger schrieb:
Ich halt mich an die Regel: "wenn die Funktion die Daten veraendert, wird ein Pointer uebergeben"....
Das heißt, "non-const-Referenzen" kommen bei Dir in Parameterlisten gar nicht vor?
Blue-Tiger schrieb:
...In der Regel kann ich vom Funktionsnamen ableiten, ob null uebergeben werden kann oder nicht. ...
Kannst Du mal ein Beispiel geben? Ich kann mir gerade keinen "organischen" Funktionsnamen vorstellen, bei dem diese Eigenschaft eines bestimmten Parameters deutlich wird.
Die "Ich-sehe-direkt-beim-Call-ob-ein-Parameter-geändert-wird"-Strategie greift mir persönlich zu kurz (eigentlich heißt sie ja: "Ich-beurteile-Veränderbarkeit-ohne-Funktionsdeklaration"-Strategie
).1.) Es gibt viel zu viele Ausnahmen - z.B.
struct A { //.... tausend Attribute int* a; int operator&(); //.... tausend weitere Attribute }; A a; f(a); // hoppla: hier sehe ich gar nicht mehr, dass A::a verändert werden könnte g(&a); // hoppla: Hier wird nur ein int übergeben und nichts verändertAußerdem ist das eine Eigenschaft der Funktion und hängt weniger der übergebenen Variablen. Deswegen hängt man sowieso (und zu Recht) an der Funktionsdeklaration.
2.) Für die Frage der Veränderbarkeit gibt es das klar definierte Konstrukt "const" ... und das sollte man IMHO auch verwenden.
Gruß,
Simon2.
-
Simon2 schrieb:
Blue-Tiger schrieb:
Ich halt mich an die Regel: "wenn die Funktion die Daten veraendert, wird ein Pointer uebergeben"....
Das heißt, "non-const-Referenzen" kommen bei Dir in Parameterlisten gar nicht vor?
mal von swap abgesehen kommen bei mir non-const-Referenzen in Parameterlisten eigentlich auch nicht vor.
Simon2 schrieb:
Blue-Tiger schrieb:
...In der Regel kann ich vom Funktionsnamen ableiten, ob null uebergeben werden kann oder nicht. ...
Kannst Du mal ein Beispiel geben? Ich kann mir gerade keinen "organischen" Funktionsnamen vorstellen, bei dem diese Eigenschaft eines bestimmten Parameters deutlich wird.
Ich würde viel eher gerne wissen, welchen Funktionen man natürlich 0 übergeben dürfen soll! Ich finde, kein indert, delete, remove, free, print, compare, find, unique, sort oder wasauchimmer muß erwarten, 0 übergeben zu bekommen.
Die "Ich-sehe-direkt-beim-Call-ob-ein-Parameter-geändert-wird"-Strategie greift mir persönlich zu kurz (eigentlich heißt sie ja: "Ich-beurteile-Veränderbarkeit-ohne-Funktionsdeklaration"-Strategie
).Das Gegenteil ist der Fall! Beim Aufrufer sehe ich sofort und ohne Funktionsdeklaration, ob es bei diesem Aufruf Veränderungsprobleme geben kann. Die Regel ist nicht für den Schreiber da, sondern für den Leser. Und den Fehlersucher/Fehlervermeider.
1.) Es gibt viel zu viele Ausnahmen - z.B.
struct A { //.... tausend Attribute int* a; int operator&(); //.... tausend weitere Attribute }; A a; f(a); // hoppla: hier sehe ich gar nicht mehr, dass A::a verändert werden könnte g(&a); // hoppla: Hier wird nur ein int übergeben und nichts verändertVielleicht müßte a in Wirklichkeit pa heißen. Vielleicht müßte das Attribut private sein und es einen const-Getter geben. Wer weiß?
Außerdem ist das eine Eigenschaft der Funktion und hängt weniger der übergebenen Variablen. Deswegen hängt man sowieso (und zu Recht) an der Funktionsdeklaration.
Und es ist wichtig genug, beim Aufrufer zu stehen! Ganz im Gegensatz zur NullKannSeinBarkeit, die mangels Existenz auch nicht so arg relevant ist.
2.) Für die Frage der Veränderbarkeit gibt es das klar definierte Konstrukt "const" ... und das sollte man IMHO auch verwenden.
Das klingt wie "Wenn der Herr geollt hätte, daß der Mensch fliegt, hätte er ihm Flügel wachsen lassen.". Unser Vorgehen unterstützt const doch.
-
volkard schrieb:
...
Simon2 schrieb:
Kannst Du mal ein Beispiel geben? Ich kann mir gerade keinen "organischen" Funktionsnamen vorstellen, bei dem diese Eigenschaft eines bestimmten Parameters deutlich wird.
Ich würde viel eher gerne wissen, welchen Funktionen man natürlich 0 übergeben dürfen soll! ...
Das ist ja interessant.
Irgendwie beantwortet das meine Frage nicht.

Aber mal auf Deine Frage:struct A { B* connectedWith; A(B* connectTo = 0) : connectedWith(connectTo) {} };Für mich steht aber auch nicht die "Nullbarkeit" im Mittelpunkt, sondern die "Umhängbarkeit" und die "Optionalität".
... und letztlich die (damit verbundene) lockerere Bindung zwischen 2 Objekten.volkard schrieb:
...Beim Aufrufer sehe ich sofort und ohne Funktionsdeklaration, ob es bei diesem Aufruf Veränderungsprobleme geben kann. ...
Die Wiederholung macht das Argument nicht besser - ich sage immer noch: Ohne Funktionsdeklaration wird man nur sehr (und für mich: zu) unzuverlässig etwas über die "Änderungspläne" einer Funktion herausbekommen.
volkard schrieb:
...Vielleicht müßte a in Wirklichkeit pa heißen. Vielleicht müßte das Attribut private sein und es einen const-Getter geben. Wer weiß?
...Welche Relevanz hat das für den Schreiber von f()?
(und wer sagt, was "...müsste.."? Das mit dem "pa" hast Du nicht ernst gemeint, oder?)volkard schrieb:
...Das klingt wie "Wenn der Herr geollt hätte, daß der Mensch fliegt, hätte er ihm Flügel wachsen lassen.". Unser Vorgehen unterstützt const doch.
Die Aussage ist aber genau anders herum: Wenn wir doch Flügel (const) haben - warum dann für Flugversuche mit den Armen (Pointer) wedeln?
Nochmal ein Beispiel:
T *************a; f(**********a); g(************a); h(&*******a);welche Funktion ändert was?
Was bedeutet eigentlich "ändert den Parameter"?
Das kann man mit "... * const * * const ...." exakt und nachvollziehbar ("wunderschön" verkneife ich mir mal in dem Zusammenang) festlegen.Gruß,
Simon2.
-
Simon2 schrieb:
Aber mal auf Deine Frage:
struct A { B* connectedWith; A(B* connectTo = 0) : connectedWith(connectTo) {} };Für mich steht aber auch nicht die "Nullbarkeit" im Mittelpunkt, sondern die "Umhängbarkeit" und die "Optionalität".
... und letztlich die (damit verbundene) lockerere Bindung zwischen 2 Objekten.Ich benutze keine Default-Parameter.
struct A { B* connectedWith; A() : connectedWith(0) {} void connect(B* connectTo) {connectedWith=cw;}//hier natürlich zeiger und 0 nicht erlaubt A() : connectedWith(B* connectTo);//hier natürlich zeiger und 0 nicht erlaubt };Umhängbarkeit deckt sich zum Teil mit der genannten Nichtkopierbarkeit und zwingt beide Lager zum Zeiger. Die Optionalität verwende ich eigentlich nur bei Such-Rückgaben. Verwendete. Ich glaube, das mache ich auch nicht mehr, sondern gebe Ranges zurück.
-
volkard schrieb:
...Ich benutze keine Default-Parameter. ...
Ich auch eher selten (hier nur der Vollständigkeit halber und zur besseren Schnittstellendoku) - spielt aber keine Rolle in diesem Zusammenhang.
volkard schrieb:
...Umhängbarkeit deckt sich zum Teil mit der genannten Nichtkopierbarkeit und zwingt beide Lager zum Zeiger. ...
Japp - das meinte ich.
Und weil es genau diese Situation ist, die einem zum Zeiger zwingt, würde ich auch genau diesen Zusammenhang damit ausdrücken wollen - und das hat nichts mit "Veränderbarkeit" zu tun.volkard schrieb:
...Ich glaube, das mache ich auch nicht mehr, sondern gebe Ranges zurück.
bei "find_first_element()" auch nicht unbedingt praktisch ...

Aber da sprechen wir wohl auch nicht von Parametern sondern von Rückgabewerten.Ich habe aber auch den Eindruck, dass Du hier diese Diskussion auf Dein spezielles Steckenpferd ("non-const Referenzen braucht/nimmt man nicht") ausweitest - die will ich nicht führen und die erwartet der Threadersteller auch nicht.
Wenn ich ihn recht verstehe, möchte er einen Überblick (also eher eine statistische Aussage) darüber, wieviele Leute seine Schnittstelle in welche Richtung interpretieren - und nicht die eine korrekte Antwort auf alle Fragen.Gruß,
Simon2.
-
Ich handhabe Schnittstellen übrigens ähnlich wie Simon2. Aber was mich interessiert, ist, wieso ihr keine/kaum Default-Parameter verwendet?
Auch betreffend Nullzeiger: Bei mir kamen schon solche Funktionen vor, wenn auch sehr selten:
T DoSomething(const S& input_param, U* output_param = 0);Also dass
output_paramgewisse Zusatzinformationen speichern könnte, die den Aufrufer im Normalfall nicht interessieren. Falls doch, übergibt man der Funktion ein zweites Argument. Erachtet ihr so eine Vorgehensweise als schlechten Stil?Ansonsten verwende ich Zeiger mit Möglichkeit zum Null-Sein in Schnittstellen, wenn ich optionale Attribute habe, also sowas wie
void Ship::SetTarget(Ship* Target);Hast du das wirklich nie, volkard, oder gehst du immer Umwege über zusätzliche Methoden?
-
Nexus schrieb:
T DoSomething(const S& input_param, U* output_param = 0);Also dass
output_paramgewisse Zusatzinformationen speichern könnte, die den Aufrufer im Normalfall nicht interessieren. Falls doch, übergibt man der Funktion ein zweites Argument. Erachtet ihr so eine Vorgehensweise als schlechten Stil?Nein, finde ich nicht. Weil man ja nur in einem Sonderfall für diese Daten interessiert. Ich denke mal, dass du hier auf einen Intersect Algorithmus anspielst. Dort finde ich das nämlich sehr passend. In erster Linie ist man ja nur mal daran interessiert, ob sich 2 Objekte schneiden. Dann möchte man vielleicht noch wissen, wo das genau passiert. Dann ist der Parameter genau passend. Würde ich auch so machen. (Habe ich glaube ich sogar auch mal).
-
Nexus schrieb:
Ansonsten verwende ich Zeiger mit Möglichkeit zum Null-Sein in Schnittstellen, wenn ich optionale Attribute habe, also sowas wie
void Ship::SetTarget(Ship* Target);Hast du das wirklich nie, volkard, oder gehst du immer Umwege über zusätzliche Methoden?
Ich kann mich jetzt nicht erinnern, sowas zu haben.
Ich nehme an, ich würdevoid Ship::SetTarget(Ship* Target);und
void Ship::ClearTarget();haben.
Ich habe ja außen schon ein if, und dann brauche ich innen nicht auch noch eins. Ich fürchte, sowas mutiert.
void Ship::SetTarget(Ship* Target) { if(Target==0) {//ClearTarget m_target=0; plan=HEALSELF|SEEKENEMY; } else {//SetTarget m_target=Target; plan=ATTACK; } }wird optimiert zu
void Ship::SetTarget(Ship* Target) { if(Target==0) {//ClearTarget plan=HEALSELF|SEEKENEMY; } else {//SetTarget plan=ATTACK; } m_target=Target; }
-
drakon schrieb:
Nein, finde ich nicht. Weil man ja nur in einem Sonderfall für diese Daten interessiert. Ich denke mal, dass du hier auf einen Intersect Algorithmus anspielst.
Danke für die Antwort. Stimmt, Intersection wäre ein gutes Beispiel, ich habe jetzt nicht einmal konkret daran gedacht.
volkard schrieb:
Ich habe ja außen schon ein if, und dann brauche ich innen nicht auch noch eins. Ich fürchte, sowas mutiert.
Innen braucht man ja nur eine If-Abfrage, wenn man mehr als eine
Target-Zuweisung macht. Was meinst du genau mit aussen?Bei einer einzelnen Funktion kann dafür die Anwendung leichter sein:
MyShip.SetTarget(AlliedShip.GetTarget());v.s.
Ship* Target = AlliedShip.GetTarget(); if (Target == 0) MyShip.ClearTarget(); else MyShip.SetTarget(Target);Hier würdest du ja kaum speziell abfragen (sowas wie
bool HasTarget() const), oder?
-
Nexus schrieb:
Was meinst du genau mit aussen?
Ich gehe davon aus, daß der Aufrufer weiß, ob SetTarget(0) oder SetTarget(echtesDing) gemacht wird.
Nexus schrieb:
Bei einer einzelnen Funktion kann dafür die Anwendung leichter sein
Jo, wäre denkbar. Aber ich kann mir nicht vorstellen, daß ich die leicher anwendbare Version übersehen hätte, wenn sowas bei mir vorgekommen wäre.
MyShip.SetTarget(AlliedShip.GetTarget());Da steckt evtl eine Bedeutungsüberladung drin, m_target!=0 heißt zugleich, daß das Schiff im Angriffsmodus ist. Ich schätze, davor hätte ich Angst und würde den Modus woanders scpeichern und genau dann, wenn Modus==ANGRIFF, dann ist m_target definiert. Evtl wäre m_target auch nur ein Attribut eines AngriffsPlan-Objekts und nicht immer optionales Schiffsattribut. Ich tue mir mit optionalen Attributen recht schwer.
-
Mir ist klar dass meine Konvention nur eine von vielen moeglichen ist, aber fuer mich hat sie bisher eigentlich immer funktioniert und erleichtert das Code-Lesen.

Allerdings gebe ich dir Recht, manchmal hantiert man mit rohen Pointern (eben weil man NULL benoetigt), und dann passierts, dass man Funktionen Pointern uebergibt, obwohl diese ihren Parameter gar nicht veraendern (War aber fuer mich Persoenlich noch nie ein Problem).Simon2 schrieb:
Blue-Tiger schrieb:
Ich halt mich an die Regel: "wenn die Funktion die Daten veraendert, wird ein Pointer uebergeben"....
Das heißt, "non-const-Referenzen" kommen bei Dir in Parameterlisten gar nicht vor?
In der Regel nicht, nein
Blue-Tiger schrieb:
...In der Regel kann ich vom Funktionsnamen ableiten, ob null uebergeben werden kann oder nicht. ...
Kannst Du mal ein Beispiel geben? Ich kann mir gerade keinen "organischen" Funktionsnamen vorstellen, bei dem diese Eigenschaft eines bestimmten Parameters deutlich wird.
addItem --> NULL ist kein Item (sonderne eben die Abwesenheit davon), also macht addItem(NULL) auch keinen Sinn.
processEntry --> NULL ist kein Entry, also macht processEntry auch keinen Sinn.
isValid --> da koennte NULL Sinn machen.
traverseTree -> dito (da Blaetter ja in der Regel mit null abgespeichert werden)
...Die "Ich-sehe-direkt-beim-Call-ob-ein-Parameter-geändert-wird"-Strategie greift mir persönlich zu kurz (eigentlich heißt sie ja: "Ich-beurteile-Veränderbarkeit-ohne-Funktionsdeklaration"-Strategie
).Solang sich jeder im Projekt an die Strategie haelt, funktionierts aber!
1.) Es gibt viel zu viele Ausnahmen - z.B.
struct A { //.... tausend Attribute int* a; int operator&(); //.... tausend weitere Attribute }; A a; f(a); // hoppla: hier sehe ich gar nicht mehr, dass A::a verändert werden könnte g(&a); // hoppla: Hier wird nur ein int übergeben und nichts verändertIch geb zu das ist mir noch nie passiert. Aber solang ich mich an die "Wenn die Funktion was aendert, wird als Pointer uebergeben" Regel halte, gaebe es den Aufruf f(a) nur dann, wenn f A::a nicht veraendert. Und operator& hab ich eigentlich noch nie ueberladen.
2.) Für die Frage der Veränderbarkeit gibt es das klar definierte Konstrukt "const" ... und das sollte man IMHO auch verwenden.
Wenn ich Code ueberfliege hab ich die Deklaration aber selten so genau im Kopf.