Parameter Passing Frage



  • Hallo,
    ich habe eine Frage zum übertragen von Argumenten, als Orientierungsvorgabe
    nehme ich dieses hier:

    https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#fcall-parameter-passing

    Verstehe ich das richtig?

    Out-Fall, keine Argument, es gilt:

    double getCalculationResult() {
      return result;
    }
    

    Out-Fall mit Argument, es gilt:

    void getCalculationResult(double &value) {
      value = result;
    }
    

    Stimmt das so? Ich bevorzuge eigentlich immer dieses hier, da ich es übersichtlicher finde:

    double getCalculationResult(double &value) {
      return result;
    }
    double result = getCalculationResult(value);
    
    1. Cheap-Fall.
    void calculate(int value) {
      ....
    }
    

    aber bei einem double schon mit const reference:

    void calculate(double const &value) {
      ....
    }
    

    Ist der Grund, warum die const-Refences dem by-value vorzuziehen ist, nur die bessere Performance?

    Danke für die Hilfe.



  • @Mond sagte in Parameter Passing Frage:

    Ist der Grund, warum die const-Refences dem by-value vorzuziehen ist, nur die bessere Performance?

    Jain. Das stimmt so nämlich oft nicht. Bei vielen Typen ist die Performance in Wirklichkeit besser wenn man "by value" übergibt.
    Und natürlich gibt es auch Fälle wo die übergebene Funktion die Adresse des Objekts kennen muss, weil dieses "eine Identität" hat.
    Und dann gibt es natürlich auch noch Fälle wo das Objekt gar nicht kopiert werden kann -- weil es halt "unkopierbar" ist.



  • @Mond sagte in Parameter Passing Frage:

    Stimmt das so? Ich bevorzuge eigentlich immer dieses hier, da ich es übersichtlicher finde:
    double getCalculationResult(double &value) {
    return result;
    }
    double result = getCalculationResult(value);

    Das ist jetzt aber eine seltsame Variante, denn du hast sowohl einen Rückgabewert als auch einen in/out-Parameter! Das solltest du vermeiden! Ich würde den double einfach per Value übergeben, sehe da keinen Grund für die Referenz.

    aber bei einem double schon mit const reference:

    Warum double schon als const&?

    Aus den von dir zitierten Guidelines:

    What is “cheap to copy” depends on the machine architecture, but two or three words (doubles, pointers, references) are usually best passed by value.



  • @wob sagte in Parameter Passing Frage:

    Aus den von dir zitierten Guidelines:

    What is “cheap to copy” depends on the machine architecture, but two or three words (doubles, pointers, references) are usually best passed by value.

    Ok, das habe ich überlesen, d.h. ein double wäre ebenfalls cheap?

    void calculate(double value) {
      // nichts zurueckgegeben
    }
    

    Wie siehts aus mit Funktionsobjekten oder Lambdas?



  • @Mond sagte in Parameter Passing Frage:

    Ok, das habe ich überlesen, d.h. ein double wäre ebenfalls cheap?

    Ja!

    Auf so einem Standard 64-Bit-Computer ist sizeof(double) = sizeof(void*) = 8. Das ist dieselbe Größe, aber bei einem Pointer/Referenz hast du dann eine zusätzliche Indirektion.

    Und selbst wenn der Wert breiter ist als ein Pointer, kann by value trotzdem besser sein. Ggf. kann der Optimizer besser optimieren und so weiter. Hängt natürlich alles vom konkreten Beispiel ab.

    Und wenn du was zurückgibst und eine Referenz übergeben willst, dann schaue, dass du eine const& nimmst, wenn möglich.



  • @wob sagte in Parameter Passing Frage:

    Und wenn du was zurückgibst und eine Referenz übergeben willst, dann schaue, dass du eine const& nimmst, wenn möglich.

    Ich bin verwirrt. In der im Link angegebenen Tabelle steht doch unter "Out (=zurückgeben)", dass dann kein Const zu nehmen ist?
    Bleibt die Frage, wann Const& zu nehmen ist (cheap to move?).



  • @Mond sagte in Parameter Passing Frage:

    @wob sagte in Parameter Passing Frage:

    Und wenn du was zurückgibst und eine Referenz übergeben willst, dann schaue, dass du eine const& nimmst, wenn möglich.

    Ich habe das wichige Wort in meinem Zitat mal fett gemacht. Konkret hatte ich mich auf dein Beispiel bezogen:

    double getCalculationResult(double &value) {

    Du gibst etwas zurück (via return), hast aber trotzdem noch einen non-const Parameter. Das sollte möglichst nicht sein, denn dann hättest du ja gleich zwei Rückgabekanäle gleichzeitig. Das ist fehleranfällig und ich würde davon abraten. Man sieht dieses Patter manchmal, wenn der Rückgabewert ein Erfolgscode ist und die eigentlichen Daten über den out-Parameter zurückkommen.

    Ich bin verwirrt. In der im Link angegebenen Tabelle steht doch unter "Out (=zurückgeben)", dass dann kein Const zu nehmen ist?

    Ja, aber du gibst in deiner Funktion ja schon via return zurück.

    Bleibt die Frage, wann Const& zu nehmen ist (cheap to move?).

    Immer dann, denn du nicht per value übergeben willst und der Parameter nicht geändert werden soll (kein out-Parameter ist).



  • Ok, danke, verstanden.
    D.h. const in der Parameterliste wird nur in folgenden Fällen benutzt:

    1. Referenz als Input und gleichzeitig ein Rückgabewert
    double getCalculationResult(const double &value) {
      return result;
    }
    double result = getCalculationResult(value);
    
    1. Referenz als Input, die nicht verändert oder zurückgegeben werden soll.
    void f1(const string& s);  // OK: pass by reference to const; always cheap
    
    1. Cheap to move / moderate to move
      ?

    2. Templates / eigene Klassen/ struct , nur In, nicht Out

    template <typename T>
    void doOperation(const T f){
      // nichts zurueck
    }
    
    1. Evtl. noch: Übergabe von const Variablen
    double calculate(const double value) {
      // nichts zurueck
    }
    const double inputValue = 5;
    calculate(inputValue);
    
    

    Stimmt das so?



  • Übergabeparameter und Rückgabewert haben nichts miteinander zu tun, die Zusammenhänge ergeben sich einfach aus der Funktionsweise der Funktion.
    Es gibt einige Faustregeln für die Übergabe von Parametern:

    Übergabe von Parametern, die nicht verändert werden, in der Funktion also nur gelesen werden:

    1. Wertetyp (bool, char, int, long, double, etc.) werden per value übergeben
    2. "billig" zu kopierende Datentypen (alles, was größenmäßig in ein CPU-Register oder Cacheline passt) werden per value übergeben
    3. alles andere per const reference
    double multiply( double f1, double f2 )
    {
       return f1 * f2;
    }
    
    struct simple_struct
    {
        double member = 0;
    }
    
    void foo( simple_struct s )
    {
       // simple_struct ist billig zu kopieren
    }
    
    struct complex_struct
    {
       std::string name;
       std::vector<std::string> zeugs;
    };
    
    void bar( complex_struct const& cs )
    {
       // complex_struct ist "teuer" zu kopieren, also per const reference übergeben
       ...
    }
    
    void baz( std::string const& s )
    {
       // std::string ist "teuer" zu kopieren, also per const reference übergeben
    }
    

    Übergabe von Parametern, die in der Funktion verändert werden

    1. Übergabe als reference
    bool some_func( double f1, double f2, double& result )
    {
       // f1 und f2 sind billig zu kopieren, Übergabe per value. In result wird das Ergebnis geschrieben, also muss
       // result als reference übergeben werden.
       if( some_condition() )
       {
          result = f1 * f2;
          return true;
       }
       return false;
    }
    

    Übergabe von optionalen Parametern, die nicht verändert werden:

    1. Übergabe als const pointer
    void foo( some_type const* param )
    {
       if( param )
       {
          param->do_something();
       }
    }
    

    Übergabe von optionalen Parametern, die verändert werden können:

    1. Übergabe als pointer
    void foo( some_type const* param )
    {
       if( param )
       {
          param->result = calculate_something();
          param->data = calculate_something_different();
       }
    }
    

    Optimierungen, die durch move Semantik möglich sind, lasse ich hier mal außen vor.


Anmelden zum Antworten