C++ Lernen - Verständnisfragen



  • Mit dem std::vector als Parameter funktioniert deine Funktion natürlich nicht mehr mit std::array. Und umgekehrt. Eigentlich sind diese Objekte aber sehr ähnlich. Daher ist es häufig sinnvoller, es so wie die stl zu machen, d.h. du schreibst eine Funktion, die zwei Iteratoren als Parameter hat. Dann kannst du auch z.B. mal das "halbe" Array verarbeiten. Häufig ist so eine Funktion mit einem ganzen vector als Parameter ganz nett zu haben, wenn du sowieso nur mit vector und immer mit dem ganzen vector arbeitest. Aber generell würde ich dir empfehlen, dir mal den algorithm-Header anzuschauen und wie die Funktionen dort Parameter behandeln. Es gibt ja z.B. auch noch std::deque usw.



  • @Finnegan ich würde generell (es sei denn es gibt im Spezialfall einen guten Grund) das std::move bei dem return weg lassen. Damit verhinderst du Copy elision und erzwingst den move. Wenn Copy elision nicht möglich ist, sollte jeder Compiler von sich aus moven.



  • @Schlangenmensch sagte in C++ Lernen - Verständnisfragen:

    @Finnegan ich würde generell (es sei denn es gibt im Spezialfall einen guten Grund) das std::move bei dem return weg lassen. Damit verhinderst du Copy elision und erzwingst den move. Wenn Copy elision nicht möglich ist, sollte jeder Compiler von sich aus moven.

    Ja, gute Idee, nicht zuletzt auch weil es noch simpler und geradliniger ist:

    #include <utility>
    ...
    std::vector<string> funktion(std::vector<string> fc_array)
    {
        // modifiziere fc_array
        ...
        return fc_array;
    }
    

    Das ist genau das, was C++ immer gebraucht hat: Dass der Code, der einem als Anfänger als erstes in den Sinn kommt, bereits eine ziemlich gute Lösung ist.



  • @wob sagte in C++ Lernen - Verständnisfragen:

    d.h. du schreibst eine Funktion, die zwei Iteratoren als Parameter hat.

    Alternativ auch ein Range-Concept aus C++20:

    #include <ranges>
    ...
    void funktion(std::ranges::input_range auto range)
    

    Leider scheint es da aber irgendwie kein fertiges Concept zu geben, mit dem man den Element-Typen z.B. auf string einschränken kann, so dass man sich selbst eins definieren müsste:

    #include <concepts>
    #include <ranges>
    #include <string>
    ...
    template <typename R, typename T>
    concept input_range_of = std::ranges::input_range<R> && std::same_as<std::ranges::range_value_t<R>, T>;
    ...
    void funktion(input_range_of<std::string> auto range)
    

    Aber das geht eventuell alles etwas zu weit für einen Anfänger, der sich gerade noch mit Arrays und Vektoren befasst. Das sei mal ein "sneak peek" auf das, was C++ noch alles zu bieten hat, um das "Wie übergebe ich ein Array?"-Problem zu lösen 😉



  • Danke für die ausführlichen Antworten. Bzgl. der Variante mit dem Return...
    Wenn ich den Parameter in der "funktion" den bearbeiteten Parameter zurückgebe und dieser call by value war, bleibt das ergebnis unverändert. Erst wenn ich by reference mache übernimmt er den neuen Wert...und es spielt dann auch überhaupt keine Rolle ob ich "return str" schreibe oder nicht...wieso also "return" schreiben?

    #include <iostream>
    #include <string>
    
    std::string funktion(std::string str)
    {
    	if (str.size() > 6) str.at(str.size()-5) = '#';
    	else str = "nope";
    	return str;
    }
    
    int main()
    {
    	std::string str= "Hammpelmann";
    	std::cout << str << "\n";
    	funktion(str);
    	std::cout << str << "\n";
    }
    
    


  • @Drgreentom Du musst den Return Wert auch verwenden 😉

    #include <iostream>
    #include <string>
    
    std::string funktion(std::string str)
    {
    	if (str.size() > 6) str.at(str.size()-5) = '#';
    	else str = "nope";
    	return str;
    }
    
    int main()
    {
    	std::string str= "Hammpelmann";
    	std::cout << str << "\n";
    	std::string str2 = funktion(str);
    	std::cout << str << "\n";
    	std::cout << str2 << "\n";
    }
    


  • @Drgreentom

    Der return-value und der Referenzparameter müssen nicht den gleichen Wert/Inhalt haben, über die Funktionssignatur legst du nur fest, wie deine Funktion aufgerufen werden soll.
    Hier ein schlechtes Beispiel, das aber das Prinzip zeigt:

    #include <string>
    
    std::string func( std::string& ref )
    {
       if( ref == "Peter" )
       {
          ref = "Meier";
          return "ok";
       }
       return "error";
    }
    
    int main()
    {
       std::string p1 = "Peter";
       std::string p2 = "Thomas";
    
       std::string const result1 = func( p1 ); // p1 ist jetzt "Meier" und result1 ist "ok"
       std::string const result2 = func( p2 ); // p2 ist immer noch "Thomas" und result2 ist "error"
    }
    

    Bei Funktionen, in denen ein Parameter per Referenz übergeben wird, kann man den Referenzparameter als Rückgabewert zurückgeben, damit lassen sich Funktionen dann schachteln.



  • Ahhhh... das macht Sinn...facepalm. Da hätte ich auch selbst drauf kommen müssen. Der Returnwert ist also der Wert der Funktion nach der Bearbeitung (oder was auch immer ich returnen will) und nicht des Parameters. Daher kann ich bestimmt auch immer nur einen Wert returnen? An void kann ich ja nichts returnen soweit ich gesehen habe.
    Bisher habe ich hauptsächlich void Funktionen genutzt, den parameter als "&" aufgerufen und direkt zurückgegeben. Also in etwa so? Beide Varianten haben das gleiche Ergebnis:

    #include <iostream>
    #include <string>
    
    void void_funktion(std::string &str)
    {
          str += str;
    }
    
    std::string str_funktion(std::string str)
    {
         str += str;
         return str;
    }
    
    int main()
    {
         std::string str_1 = "Hallo ";
         std::cout << str_1 << "\n";
         
         void_funktion(str_1);
         std::cout << str_1 << " mit Void_funktion \n";
         
         std::string str_2 = str_funktion(str_1);
         std::cout << str_2 << " mit Str_funktion \n";
    }
    

    Spricht etwa dagegen so mit den void´s zu arbeiten?



  • Das Problem bei Referenzparametern ist, dass man nicht sofort sieht, dass der Aufrufparameter verändert wird. Bei der Benutzung von Rückgabewerten ist das sofort klar, wenn man die Zuweisung liest. Beim Programmieren ist es nicht nur wichtig, dass das Programm korrekt ist, sondern dass der Quellcode ausdrückt, welche Absicht der Programmierer bei der Implementierung der Funktion hat. Bei kleinen Übungsaufgaben ist das egal, aber bei größeren Projekten möchte man auf Funktionen, die ihre Aufgabe durch Seiteneffekte erfüllen, lieber nicht haben.

    void f1( std::string& ref )
    {
      ...
    }
    
    std::string f2( std::string const& val )
    {
       ...
    }
    
    int main()
    {
       std::string p1 = "Peter";
       std::string p2 = "Thomas";
    
       f1( p1 );      // nicht ersichtlich, dass p1 ggf. verändert wird
       p2 = f2( p2 ); // deutlich, dass p2 verändert wird
    }
    


  • @DocShoe sagte in C++ Lernen - Verständnisfragen:

    Das Problem bei Referenzparametern ist, dass man nicht sofort sieht, dass der Aufrufparameter verändert wird.

    OT: Genau! Deshalb finde ich es z.B. in C# sehr cool, dass man beim Aufruf "ref" oder "out" angeben muss.



  • Das es da unübersichtlich wird, da geben ich dir Recht...
    Ich habe gerade schon über 2000 Zeilen nur mit voids an einem Programm geschrieben (~30 Funktionen)...ich habe sehr darauf geachtet die Funktionen so zu bennen, das ich daraus folgern kann was sie mit den Werten macht bzw was sie mir zurück gibt. Aber trotzdem ist das bereits sehr unübersichtlich.
    Ich schreibe die letzten Zeilen erstmal ferig und hoffe, dass ich das nun hier gerade erlernte bei der nächsten Revision anwenden kann ohne das ganze Programm zu schießen!



  • Ein weiteres Argument ist Komposition: output(f3(f2(f1(input()))). Mit Refernzparametern wird das eventuell unübersichtlicher und man braucht auf jeden Fall noch eine Variable für die Zwischenergebnisse, während man ansonsten vielleicht einfach nur temporäre Werte durchreichen könnte.



  • @DocShoe sagte in C++ Lernen - Verständnisfragen:

    Bei kleinen Übungsaufgaben ist das egal, aber bei größeren Projekten möchte man auf Funktionen, die ihre Aufgabe durch Seiteneffekte erfüllen, lieber nicht haben.

    Das Wort Seiteneffekte ist hier fehl am Platz, da nur die übergebenen Werte verändert werden und nichts anderes. Von Seiteneffekten spricht man üblicherweise, wenn etwas anderes verändert als die Parameter der Funktion wird z.B. globale Variablen. Womit wir beim Thema Benennung von Funktionen angelangt wären. Der Funktionsname sollte klar beschreiben was die Funktion macht.



  • @Drgreentom Generell würde ich dir noch raten, möglichst viel const zu verwenden. Wenn sich Variablen nicht ändern, ist es einfacher zu sehen, wofür die Variablen stehen. Vor allem nicht: "hier in Zeile 1 steht x für diese Daten, in Zeile 10 steht x für diese anderen Daten".

    Durch const vermeidest du das. Da du eine const-Variable aber nicht ändern kannst, funktioniert logischerweise auch der call-by-reference nicht zum Ändern der Variable. Du müsstest eine const& als Parameter nehmen und könntest somit die Variable in der Funktion auch nicht ändern. Wenn du aber stattdessen einfach eine neuen Wert zurückgibst, der sich aus altem const-Wert + Änderung ergibt, dann hast du dieses Problem nicht und dein Programm wird tendenziell klarer.

    Natürlich muss man schauen, ob das so immer sinnvoll ist. Wenn du große Objekte hast, ist es möglicherweise ineffizient, immer zu kopieren und eine direkte Änderung kann dann sinnvoller sein.

    Jedenfalls: sofern nichts dagegen spricht, bevorzuge const-Variablen und return-Werte anstelle von Reference-Parametern ohne const.



  • @wob sagte in C++ Lernen - Verständnisfragen:

    Generell würde ich dir noch raten, möglichst viel const zu verwenden. Wenn sich Variablen nicht ändern, ist es einfacher zu sehen, wofür die Variablen stehen. Vor allem nicht: "hier in Zeile 1 steht x für diese Daten, in Zeile 10 steht x für diese anderen Daten".
    Durch const vermeidest du das. Da du eine const-Variable aber nicht ändern kannst, funktioniert logischerweise auch der call-by-reference nicht zum Ändern der Variable. Du müsstest eine const& als Parameter nehmen und könntest somit die Variable in der Funktion auch nicht ändern. Wenn du aber stattdessen einfach eine neuen Wert zurückgibst, der sich aus altem const-Wert + Änderung ergibt, dann hast du dieses Problem nicht und dein Programm wird tendenziell klarer.
    Natürlich muss man schauen, ob das so immer sinnvoll ist. Wenn du große Objekte hast, ist es möglicherweise ineffizient, immer zu kopieren und eine direkte Änderung kann dann sinnvoller sein.
    Jedenfalls: sofern nichts dagegen spricht, bevorzuge const-Variablen und return-Werte anstelle von Reference-Parametern ohne const.

    Danke nochmal für den Hinweis, ich versuch das mit den const Variablen bereits zu beachten. Gerade Vectoren die viele Strings enthalten (z.b. eingelesene Dateien) habe ich fast immer als const referenz aufgerufen um mir das Kopieren zu sparen - sofern nur gelesen werden sollte. By Value habe ich bisher sogut wie nie genutzt. Jedoch gerade der letzte Punkt war auch aus den letzten Antworten der entscheidene Hinweis, für meine nächsten Projekte - jetzt wo ich das mit dem Return verstanden habe 🙂


Anmelden zum Antworten