Stilfrage - pass per Value/const reference?



  • Hallo zusammen,

    ich hatte gerade eine angeregte Diskussion über die Übergabe von Parametern.
    Welche Variante zieht ihr vor, und warum?

    Variante 1:

    std::string f( std::string s )
    {
       std::transform( s.begin(), s.end(), s.begin(), SomeFunctor() );
       return s;
    }
    

    Variante 2:

    std::string f( const std::string& s )
    {
       std::string t = s;
       std::transform( t.begin(), t.end(), t.begin(), SomeFunctor() );
       return t;
    }
    

    Edit:
    f ist hier eine freie Funktion, die den Inhalt von s möglicherweise ändert, hängt halt vom Funktor ab. Ein konkreter Funktor ist z.B. die Ersetzung eines Zeichens durch ein anderes oder die Umwandlung zwischen Groß- und Kleinschrift. Es ist sicher, dass f den string s immer verändern können muss.

    Edit 2:
    Ich habe mich noch nicht ausgiebig mit C++11 beschäftigt, kann ich den Rückgabeparameter als rvalue referenzen und std::move zurückgeben?

    std::string&& f( std::string s )
    {
       return std::move( s );
    }
    

    bzw.

    std::string&& f( const std::string s )
    {
       std::string t = s;
       return std::move( t );
    }
    

    Oder ist das wegen RVO sowieso egal?



  • im größeren Kontext ziehe ich Variante II vor, weil dadurch nicht irgendwelche Implementierungsdetails in die Schnittstelle reinrutschen. Z.b. könnte bei polymorphen Methoden ja die eine Methode in der Basisklasse das so brauchen (also die Kopie vom Argument), während die andere Methode in der abgeleiteten Klasse das nicht benötigt und tatsächlich die Referenz ausreichen würde. Dann hätte man eine Kopie zu viel erstellt, obwohl gar nicht nötig.

    wenn ich nur eine kleine Funktion schreibe die auch nur recht lokal verwendet wird oder gar nur in einem Testprojekt verwendet wird, na dann mache ich natürlich auch Variante I.



  • Definitiv Variante 1: Diese ist nicht nur eine Zeile kürzer, sondern kann auch davon profitieren, wenn der Funktion eine rvalue reference übergeben wird.
    In diesem Fall wird nämlich der Move-Konstruktor von std::string aufgerufen und man spart sich eine ansonsten zwingende Kopie, falls der
    Compiler nach Optimierungen nicht ohnehin alles direkt in der Variable macht, welcher man das Ergebnis von f() zuweist.

    Finnegan

    P.S.: Falls man den übergebenen String nur lesen muss und keine Kopie benötigt - z.B. bei einer Funktion, welche vielleicht die Häufigkeit des Buchstabens 'e'
    im String ermittelt, macht natürlich die const& mehr Sinn. Allerdings würde ich mich dabei nicht davon beeinflussen lassen, dass die Funktion ein einer
    abgeleiteten Klasse irgendwann mal keine Kopie benötigen könnte, sondern so etwas erst "optimieren", wenn der Fall auch tatsächlich eintritt und die Perfomance
    auch messbar davon proditiert.



  • Finnegan schrieb:

    sondern so etwas erst "optimieren", wenn der Fall auch tatsächlich eintritt und die Perfomance auch messbar davon proditiert.

    Und aus diesem Grund würde ich immer die zweite Variante nehmen. Das ist die saubere Variante mit einer sauberen Schnittstelle. Was die Funktion intern macht, ist meist zweitrangig und kann sich ständig ändern. Und ob der Aufrufer den Parameter tatsächlich moven will, ist auch eine ganz andere Frage.



  • Mechanics schrieb:

    Finnegan schrieb:

    sondern so etwas erst "optimieren", wenn der Fall auch tatsächlich eintritt und die Perfomance auch messbar davon proditiert.

    Und aus diesem Grund würde ich immer die zweite Variante nehmen. Das ist die saubere Variante mit einer sauberen Schnittstelle. Was die Funktion intern macht, ist meist zweitrangig und kann sich ständig ändern. Und ob der Aufrufer den Parameter tatsächlich moven will, ist auch eine ganz andere Frage.

    Ist das wirklich dein Ernst? Einen String "by value" engegenzunehmen ist für dich eine "Optimierung" die man erstmal rechtfertigen muss?
    Ich würde behaupten das die natürlichste Variante, die jeder Anfänger als erstes implementiert - und obendrein ist sie sogar effizienter für die Klasse von Funktionen,
    welche für ihre Arbeit eine Kopie des Parameters benötigen. Ich sehe da eher die Verwendung einer Referenz als einen Optimierungsschritt - wenn auch ein natürlicher,
    für den eine logische Herleitung meist ausreicht (die aber in diesem Fall nicht greift).

    Ferner verstehe ich auch das Argument der "sauberen Schnittstelle" in diesem Kontext nicht. "by value" ist doch wesentlich abgekapselter als eine Referenz:
    Der String-Parameter gehört der Funktion ganz allein und ist Teil ihres ganz persönlichen Stack-Frames. Auch wenn bei einer const& nur lesend auf "funktionsexternen"
    Speicher zugegriffen wird, würde ich einen "by value"-Parameter als deutlicher getrennte Schnittstelle bezeichnen.

    Finnegan



  • DocShoe schrieb:

    ...
    bzw.

    std::string&& f( const std::string s )
    {
       std::string t = s;
       return std::move( t );
    }
    

    Oder ist das wegen RVO sowieso egal?

    Nicht nur egal sondern oft sogar schädlich. Ein return std::move(...); verhindert hier NRVO, da diese nicht für Referenzen durchgeführt werden kann,
    und std::move() effektiv ein "Cast" zu einer RValue-"Referenz" ist ( string&& in diesem Fall).

    Wie bereits erwähnt, meiner Meinung nach ist hier die Geradeaus-Hirnlos-Variante (1.) die effizienteste, eleganteste und kürzeste 😃 - genau wie es sein sollte.
    Allerdings: Wenn die Funktion keine "Arbeitskopie" benötigt spart man sich mit const& ein kopieren des Strings - es hängt also davon ab was die Funktion letztendlich mit dem Parameter macht.

    Finnegan



  • Finnegan schrieb:

    es hängt also davon ab was die Funktion letztendlich mit dem Parameter macht.

    Den Verdacht hatte ich schon immer. Danke für die Bestätigung.





  • Mooooment:

    Was er da unten in seinem Start Beitrag schreibt, also das explizite fordern einer RValue Referenz als Rückgabe Type, ist doch gar nicht nötig oder?

    die string Klasse verwendet den Move Konstruktor doch im Fall eines lokal erzeugten Objekts innerhalb der Funktion automatisch oder nicht?



  • Mechanics schrieb:

    Und aus diesem Grund würde ich immer die zweite Variante nehmen. Das ist die saubere Variante mit einer sauberen Schnittstelle. Was die Funktion intern macht, ist meist zweitrangig und kann sich ständig ändern.

    Versteh ich nicht.

    Wenn ich eine Funktion mit const& Parameter sehe, dann erwarte ich dass die Funktion keine Kopie anlegt. Wenn dann trotzdem eine Kopie angelegt wird, fände ich das sehr verwirrend und unintuitiv.

    Eine saubere Schnittstelle zeichnet für mich auch aus dass sie so eindeutig wie möglich die Funktion dokumentiert - sonst könnte ich ja auch gleich überall const weglassen mit der Begründung dass vielleicht irgendwann mal der Parameter doch geändert werden können muss.



  • happystudent schrieb:

    Mechanics schrieb:

    Und aus diesem Grund würde ich immer die zweite Variante nehmen. Das ist die saubere Variante mit einer sauberen Schnittstelle. Was die Funktion intern macht, ist meist zweitrangig und kann sich ständig ändern.

    Versteh ich nicht.

    Wenn ich eine Funktion mit const& Parameter sehe, dann erwarte ich dass die Funktion keine Kopie anlegt. Wenn dann trotzdem eine Kopie angelegt wird, fände ich das sehr verwirrend und unintuitiv.

    Eine saubere Schnittstelle zeichnet für mich auch aus dass sie so eindeutig wie möglich die Funktion dokumentiert - sonst könnte ich ja auch gleich überall const weglassen mit der Begründung dass vielleicht irgendwann mal der Parameter doch geändert werden können muss.

    wenn du eine Vererbungshierarchie hast und exakt eine abgeleitete Klasse jetzt eine Kopie braucht - na dann würde ich keinesfalls überall sonst das const& wegnehmen, sondern stattdessen in der einen Funktionsdefinition eine Kopie anlegen.

    Auch wenn bei einer const& nur lesend auf "funktionsexternen"
    Speicher zugegriffen wird, würde ich einen "by value"-Parameter als deutlicher getrennte Schnittstelle bezeichnen.

    du weißt ja nicht, wie die String Klasse implemntiert ist. Angenommen, die ist intern mit einem Referenzzähler implementiert, na dann greifst du auch auf die Originaldaten zu - genau wie bei const& (natürlich auch nur lesend).

    Da hier immer wieder das Argument von wegen "Compiler optimiert ja eh" kommt:
    das mag auf moderne Compiler zutreffen, aber bedenkt bitte auch, dass es für viele Plattformen nur alte C++ Compiler gibt, und die optimieren viel weniger als man vielleicht hofft.



  • gdfgdfgd schrieb:

    Auch wenn bei einer const& nur lesend auf "funktionsexternen"
    Speicher zugegriffen wird, würde ich einen "by value"-Parameter als deutlicher getrennte Schnittstelle bezeichnen.

    du weißt ja nicht, wie die String Klasse implemntiert ist. Angenommen, die ist intern mit einem Referenzzähler implementiert, na dann greifst du auch auf die Originaldaten zu - genau wie bei const& (natürlich auch nur lesend).

    Da hier immer wieder das Argument von wegen "Compiler optimiert ja eh" kommt:
    das mag auf moderne Compiler zutreffen, aber bedenkt bitte auch, dass es für viele Plattformen nur alte C++ Compiler gibt, und die optimieren viel weniger als man vielleicht hofft.

    Performance hat nichts mit einer "sauberen Schnittstelle" zu tun, um die es bei diesem Argument ging.
    Eine solche ist nach meinem Verständnis weitestgehend abgekapselt von Implementations-Details.
    Mit deinem Argument reduzierst du sogar noch gedanklich die Abkapselung, da jetzt die Verwendung einer
    Referenz sogar eine compilerspezifische Eigenheit wiederspiegelt.

    Ferner basiert mein Agrument für "by value" mitnichten auf Compiler-Optimierungen wie NRVO und dergleichen.
    Die Tatsache, dass std::string in C++11 einen Move-Kostruktor hat der in diesem Fall greift ist dafür völlig ausreichend.
    Egal wie "schlecht" der Compiler sonst optimiert.

    Finngean



  • gdfgdfgd schrieb:

    wenn du eine Vererbungshierarchie hast und exakt eine abgeleitete Klasse jetzt eine Kopie braucht - na dann würde ich keinesfalls überall sonst das const& wegnehmen, sondern stattdessen in der einen Funktionsdefinition eine Kopie anlegen.

    Hab ich aber nicht, es ging doch um eine freie Funktion?

    DocShoe schrieb:

    f ist hier eine freie Funktion



  • Finnegan schrieb:

    Performance hat nichts mit einer "sauberen Schnittstelle" zu tun, um die es bei diesem Argument ging.

    Ferner basiert mein Agrument für "by value" mitnichten auf Compiler-Optimierungen wie NRVO und dergleichen.
    Die Tatsache, dass std::string in C++11 einen Move-Kostruktor hat der in diesem Fall greift ist dafür völlig ausreichend.
    Egal wie "schlecht" der Compiler sonst optimiert.

    zu 1: eine saubere Schnittstelle ist aber const& und nicht per-value. Wenn ich per-value bei Objekten sehe, denke ich mir: mmmh, hat da jemand vergessen const& zu verwenden. Wenn in der Funktion dann eine Kopie erzeugt wird, soll das so sein, ist mir als Anwender dieser Funktion egal.

    zu 2: wer redet von C++11? Für sehr viele Plattformen gibts C++ (03) und das wars. Und genau diese Compiler sind dann oft auch nicht besonders gut im Optimieren. D.h. wenn ich performanten Code will, dann muss ich dies auch explizit ausprogrammieren und nicht hoffen dass der Compiler mir da hilft.



  • happystudent schrieb:

    Wenn ich eine Funktion mit const& Parameter sehe, dann erwarte ich dass die Funktion keine Kopie anlegt. Wenn dann trotzdem eine Kopie angelegt wird, fände ich das sehr verwirrend und unintuitiv.

    Ich erwarte hier gar nichts. Das sind für mich Implementierungsdetails, die sich ständig ändern können (und sich auch ständig ändern, ohne dass die Schnittstelle angepasst wird). Ich erwarte von der Schnittstelle, dass keine Kopie erstellt wird, wenn keine benötigt wird (deswegen die Referenz) und dass das reingegebene Objekt nicht verändert wird (deswegen const). Ob die Funktion jetzt grad doch eine Kopie benötigt, ist mir völlig egal.
    Das weiß man meist auch nicht. Eine Funktion kann viele ifs haben und Unterfunktionen aufrufen. Manche Codepfade brauchen eine Kopie, andere nicht. Wenn ich "Funktion" schreibe, muss ich die zwei Tage später vielleicht wieder umbauen, weil ich was ähnliches auch an einer anderen Stelle brauche und den Code deswegen rausziehe. Und ob dabei überall eine Kopie benötigt wird, ab und zu eine Kopie benötigt wird, oder nie eine Kopie benötigt wird, kann man ohne viel Aufwand überhaupt nicht nachvollziehen.
    Und ich rede nicht von kleinen Demoprogrämmchen, die man alleine schreibt, sondern von großer Software, die von dutzenden Mitarbeitern über Jahrzehnte entwickelt und gepflegt wird. Und ich geht stark davon aus, dass pass by valie insgesamt zu sehr viel mehr Kopien als nötig führen würde.



  • Mechanics schrieb:

    Das weiß man meist auch nicht. Eine Funktion kann viele ifs haben und Unterfunktionen aufrufen. Manche Codepfade brauchen eine Kopie, andere nicht.

    wobei man eine einzelne Funktion, die mit if-Kaskaden in Bildschirmbreite gespickt ist und vor Unterfunktionsaufrufen strotzt, vielleicht erst einmal besser faktorisieren sollte.



  • gdfgdfgd schrieb:

    zu 1: eine saubere Schnittstelle ist aber const& und nicht per-value. Wenn ich per-value bei Objekten sehe, denke ich mir: mmmh, hat da jemand vergessen const& zu verwenden. Wenn in der Funktion dann eine Kopie erzeugt wird, soll das so sein, ist mir als Anwender dieser Funktion egal.

    Da ist diese Behauptung schon wieder. Warum soll "by value" bitteschön eine weniger "saubere" Schnittstelle sein? Weil du "denkst" das da was fehlt ist kein wirklicher Grund.
    BTW.: Ich habe nie behauptet dass "by value" "sauberer" sein soll (was man auch immer darunter verstehen soll). Ich habe lediglich argumentiert, dass wenn man denn wirklich
    so spitzfindig sein will, "by value" eher das Attribut "sauber" zustünde.

    gdfgdfgd schrieb:

    zu 2: wer redet von C++11? Für sehr viele Plattformen gibts C++ (03) und das wars. Und genau diese Compiler sind dann oft auch nicht besonders gut im Optimieren.

    Ich werde mich nicht auf eine solche sich im Kreis drehende Diskussion einlassen, nur weil ich nicht besonders hervorgehoben habe, dass sich meine Aussage nicht auf Algol 48 bezieht,
    da diese Sprache keine Move-Konstruktoren kennt. Ich erwarte dass man diese Schlussfolgerung selbständig ziehen kann und in diesem Fall eine Referenz verwendet.

    gdfgdfgd schrieb:

    D.h. wenn ich performanten Code will, dann muss ich dies auch explizit ausprogrammieren und nicht hoffen dass der Compiler mir da hilft.

    Man muss zwar nicht jeden Kleinkram optimieren, aber man muss dem Compiler auch nicht unbedingt explizit ein Move verbieten, indem man ihn zwingt, ein rvalue an eine const&
    zu binden, von welcher er nicht moven darf. Auch hier: Diese Aussage bezieht sich auf modernes C++. Wenn du das nicht verwendest, darfst du das gerne über einen Output-Parameter o.ä.
    "explizit ausprogrammieren", oder was auch immer du für eine Technik anwendest, um die zusätlichen Kopien einzusparen - was du mit modernem C++ übirgens geschenkt bekommst,
    wenn du in diesem speziellen Fall "by value" übergibst.

    Finnegan



  • klassenmethode schrieb:

    Mechanics schrieb:

    Das weiß man meist auch nicht. Eine Funktion kann viele ifs haben und Unterfunktionen aufrufen. Manche Codepfade brauchen eine Kopie, andere nicht.

    wobei man eine einzelne Funktion, die mit if-Kaskaden in Bildschirmbreite gespickt ist und vor Unterfunktionsaufrufen strotzt, vielleicht erst einmal besser faktorisieren sollte.

    Du kannst überall reininterpretieren, was du willst. Und was ist, wenns nur ein if ist, reicht das nicht? Und sind Unterfunktionen kein Indiz dafür, dass die Funktion schon "besser faktorisiert" wurde und nicht alles selber macht?



  • Finnegan schrieb:

    gdfgdfgd schrieb:

    zu 1: eine saubere Schnittstelle ist aber const& und nicht per-value. Wenn ich per-value bei Objekten sehe, denke ich mir: mmmh, hat da jemand vergessen const& zu verwenden. Wenn in der Funktion dann eine Kopie erzeugt wird, soll das so sein, ist mir als Anwender dieser Funktion egal.

    Da ist diese Behauptung schon wieder. Warum soll "by value" bitteschön eine weniger "saubere" Schnittstelle sein? Weil du "denkst" das da was fehlt ist kein wirklicher Grund.
    BTW.: Ich habe nie behauptet dass "by value" "sauberer" sein soll (was man auch immer darunter verstehen soll). Ich habe lediglich argumentiert, dass wenn man denn wirklich
    so spitzfindig sein will, "by value" eher das Attribut "sauber" zustünde.

    gdfgdfgd schrieb:

    zu 2: wer redet von C++11? Für sehr viele Plattformen gibts C++ (03) und das wars. Und genau diese Compiler sind dann oft auch nicht besonders gut im Optimieren.

    Ich werde mich nicht auf eine solche sich im Kreis drehende Diskussion einlassen, nur weil ich nicht besonders hervorgehoben habe, dass sich meine Aussage nicht auf Algol 48 bezieht,
    da diese Sprache keine Move-Konstruktoren kennt. Ich erwarte dass man diese Schlussfolgerung selbständig ziehen kann und in diesem Fall eine Referenz verwendet.

    gdfgdfgd schrieb:

    D.h. wenn ich performanten Code will, dann muss ich dies auch explizit ausprogrammieren und nicht hoffen dass der Compiler mir da hilft.

    Man muss zwar nicht jeden Kleinkram optimieren, aber man muss dem Compiler auch nicht unbedingt explizit ein Move verbieten, indem man ihn zwingt, ein rvalue an eine const&
    zu binden, von welcher er nicht moven darf. Auch hier: Diese Aussage bezieht sich auf modernes C++. Wenn du das nicht verwendest, darfst du das gerne über einen Output-Parameter o.ä.
    "explizit ausprogrammieren", oder was auch immer du für eine Technik anwendest, um die zusätlichen Kopien einzusparen - was du mit modernem C++ übirgens geschenkt bekommst,
    wenn du in diesem speziellen Fall "by value" übergibst.

    Finnegan

    Wir werden es ohnehin nicht klären können, was "richtig" ist, weil beide Varianten funktionieren.
    Ich habe meine Meinung dargelegt, welche sich aus meiner Erfahrung mit nicht nur modernen Compilern ergibt und meiner Erfahrung mit typischen Fehlern in großen Codebasen ergibt: und da ist eine per-value Übergabe größerer Objekte nunmal oft nicht gewünscht gewesen (vom Ersteller dieser Codezeilen), weswegen ich bei per-value Übergaben erstmal genauer hinschaue, ggf. auch die Implementierung mir näher ansehe.



  • gdfgdfgd schrieb:

    Wir werden es ohnehin nicht klären können, was "richtig" ist, weil beide Varianten funktionieren.
    Ich habe meine Meinung dargelegt, welche sich aus meiner Erfahrung mit nicht nur modernen Compilern ergibt und meiner Erfahrung mit typischen Fehlern in großen Codebasen ergibt: und da ist eine per-value Übergabe größerer Objekte nunmal oft nicht gewünscht gewesen (vom Ersteller dieser Codezeilen), weswegen ich bei per-value Übergaben erstmal genauer hinschaue, ggf. auch die Implementierung mir näher ansehe.

    Eins ist auf jeden Fall klar: Wie fast immer gibt es keine einzige richtige Antwort 😃
    Es stimmt schon dass es immer Sonderfälle und jede Menge alten Code gibt, bei denen man mit einer anderen Strategie besser fährt.
    Der Grund, weshalb ich das "by value" hier so vehement verteidige ist, weil die vom Threadersteller gepostete Funktion gerade ein
    Paradebeispiel dafür ist, wie mit modernem C++ ausgerechnet die einfachste Lösung den besten Code generieren kann.
    Da schimmert ein wenig die wesentlich simplere Sprache durch, die laut Stroustrup irgendwie tief in C++ vergraben liegen soll.

    Ich möchte auch nochmal hervorheben, dass ich nicht sage, dass "by value" immer die erste Wahl sein sollte. Bloss nicht! Wie mehrfach erwähnt,
    bezieht sich das nur auf Funktionen die ohnehin eine Kopie des Objekts machen müssen, da sie in dessen Innereien herumwursteln.
    Hier kann man die Kopie auch genauso gut von einem Parameter-Konstruktor machen lassen und profitiert dabei noch zusätzlich,
    wenn die Funktion mit einem Temporary aufgerufen wird und die Klasse einen Move-Konstruktor hat - wenn nicht, auch egal, denn
    die Kopie wird ja sowieso benötigt. Dabei ist es dann auch egal, ob es teuer ist, das Objekt zu kopieren, denn, wer hätte es gedacht:
    die Kopie wird ja sowieso benötigt 😃

    Wird keine Kopie benötigt (Beispiel: Zähle die Buchstaben 'e' in String), dann ja, bitte! Immer gerne eine const& !

    Finnegan


Log in to reply