Eigene Vektorenklasse (Fragen, Verbesserungen)



  • Hallo zusammen,

    zurzeit befinde ich mich im Aufbau einer Vektoren-Klasse.
    Ich möchte hiermit einmal fragen, ob dies so schonmal ganz gut aussieht und gerne auch
    Verbesserungsvorschläge annehmen.

    Außerdem hätte ich noch eine Frage bezüglich const hinter meiner Funktion "operator+".
    Hier funktioniert der Aufruf mit const, aber bei meinem operator "+=" funktioniert er nicht, obwohl es ja eigentlich
    nichts anderes ist oder vertue ich mich da?

    Außerdem habe ich mal ein bisschen nachgeforscht und += wird mit einer Referenz an den Methodenaufruf
    zurückgegeben und bei + ist das nicht der Fall.

    Gibt es hierfür einen Grund?

    m_Array ist einfach ein std::array<float, 3>
    
    const float& Vector3D::getX() const {
    	return m_Array[0];
    }
    
    const float& Vector3D::getY() const {
    	return m_Array[1];
    }
    
    const float& Vector3D::getZ() const {
    	return m_Array[2];
    }
    
    const float& Vector3D::isZero() const {
    	return m_Array[0] == 0 && m_Array[1] == 0 && m_Array[2] == 0;
    }
    
    float& Vector3D::operator[](std::size_t n) {
    	switch (n) {
    	case 1: m_Array[0];
    	case 2: m_Array[1];
    	case 3: m_Array[2];
    	default: throw new std::out_of_range("Vector3D::operator[]: index out of bounds!");
    	}
    }
    
    Vector3D& Vector3D::operator=(const Vector3D& v){
    	m_Array[0] = v.getX();
    	m_Array[1] = v.getY();
    	m_Array[2] = v.getZ();
    
    	return *this;
    }
    
    //Warum funtkioniert hier kein const?
    Vector3D& Vector3D::operator+=(const Vector3D& v){
    	m_Array[0] += v.getX();
    	m_Array[1] += v.getY();
    	m_Array[2] += v.getZ();
    
    	return *this;
    }
    
    //Warum funktioniert hier const?!
    Vector3D Vector3D::operator+(const Vector3D& v) const {
    	m_Array[0] + v.getX();
    	m_Array[1] + v.getY();
    	m_Array[2] + v.getZ();
    
    	return *this;
    }
    


  • @unkwnusr sagte in Eigene Vektorenklasse (Fragen, Verbesserungen):

    Ich möchte hiermit einmal fragen, ob dies so schonmal ganz gut aussieht und gerne auch
    Verbesserungsvorschläge annehmen.

    • Implementiere die Funktionen inline im Header file.
    • Ich würde bei den Parametern und auch bei den Returntypen keine Referenzen verwenden. Das wird alles eher langsamer machen als schneller.

    Außerdem hätte ich noch eine Frage bezüglich const hinter meiner Funktion "operator+".
    Hier funktioniert der Aufruf mit const, aber bei meinem operator "+=" funktioniert er nicht, obwohl es ja eigentlich
    nichts anderes ist oder vertue ich mich da?

    Naja, der operator += ändert das "*this" Objekt - und kann daher nicht const sein. Der operator + dagegen ändert das "*this" Objekt nicht.

    Außerdem habe ich mal ein bisschen nachgeforscht und += wird mit einer Referenz an den Methodenaufruf
    zurückgegeben und bei + ist das nicht der Fall.

    Gibt es hierfür einen Grund?

    Der Grund warum man das üblicherweise so macht, ist weil es dem Verhalten der "eingebauten" Operatoren entspricht.
    Angenommen a und b sind int Variablen, dann ist das Ergebnis von a + b ein neues, temporäres int Objekt. Das Ergebnis von a += b hingegen ist eine Referenz auf a.

    Und wenn man möchte dass das bei den eigenen Operatoren auch so ist, dann muss man eben auch bei + ein Objekt zurückgeben und bei += eine Referenz auf den linken Operanden.


  • Mod

    Bei += veränderst du das Objekt, es kann also nicht const sein. const int i=0; i+=1; geht ja auch nicht. Bei + wird ein neues Objekt erzeugt, die beiden Objekte die addiert werden, können also const sein. Du machst es bei dir aber falsch, dein + enthält 3 Anweisungen, die alle nichts tun.

    Zweiseitige Operatoren wie + implementiert man übrigens besser als freie Funktionen, weil dann ggf. Konvertierungen auf beiden Seiten gleich greifen. Sonst bekommt man unintuitive Effekte, wie dass für a + b eine andere Funktion aufgerufen wird als für b + a.



  • Wow.. das ging schnell 😃
    Vielen Dank für die schnellen Antworten.
    Die Erklärung mit dem "+" und "+=" habe ich sofort verstanden und ja... ich habe wohl nicht ganz drüber nachgedacht.

    Kurze Fragen noch zu Thema "inline".
    Warum genau ist es vorhergesehen inline an den Anfang der Methode zu setzen?
    Habe mich da mal ein wenig reingelesen und für mich hört es sich so raus, dass inline
    nicht die Methode ansich aufruft sondern den Inhalt quasi "rauskopiert", was ein wenig Performance bringen soll.
    Richtig?


  • Mod

    @unkwnusr sagte in Eigene Vektorenklasse (Fragen, Verbesserungen):

    Kurze Fragen noch zu Thema "inline".
    Warum genau ist es vorhergesehen inline an den Anfang der Methode zu setzen?
    Habe mich da mal ein wenig reingelesen und für mich hört es sich so raus, dass inline
    nicht die Methode ansich aufruft sondern den Inhalt quasi "rauskopiert", was ein wenig Performance bringen soll.
    Richtig?

    Jain. Das ist korrekt, dass dies die Absicht hinter der Existenz des Schlüsselwortes ist. Praktisch entscheidet das der Compiler aber selber schon ganz gut, und trifft seine eigene Entscheidungen, ganz unabhängig von dem inline Schlüsselwort.

    Aber das inline hat noch einen anderen Nebeneffekt und den will man eigentlich haben: inline-Funktionen dürfen mehrmals im Programm definiert sein, sofern sie überall die gleiche Definition haben. Normalerweise ist es ja ein Fehler, eine Funktion mehrmals im Programm definiert zu haben. Warum sollte man eine Funktion mehrmals definieren wollen? Header! Weil die Header in mehreren Sourcen eingebunden sein können, müssen Funktionsdefinitionen, wenn sie in Headern stehen, inline sein. (Funktionen die in einer Klassendefiniton direkt drin stehen, sind übrigens automatisch inline: class Foo { void dies_ist_inline(){//wenn hier der Code steht} };).

    Was schreibt man gerne in Headern für Funktionen? Kurze, triviale Funktionen, also genau solche, von denen man gerne hätte, dass der Compiler sie direkt einsetzt. Und so schließt sich der Kreis.



  • @unkwnusr
    Wie @SeppJ ja schon erklärt hat heisst inline nicht automatisch dass der Compiler den Funktionsaufruf inlinen ("rauskopieren") wird. Umgekehrt heisst es auch nicht der Compiler ohne inline kein inlining machen wird. Es ist bloss formal nötig wenn man Funktionen in Header-Files definiert.

    Und die Funktion im Header-File zu definieren ist das was hier mMn. Sinn macht. Denn es ermöglicht dem Compiler die Funktion zu inlinen. (Genaugenommen können viele Compiler auch Funktionen inlinen die in .cpp Files definiert sind, aber dieses Feature ist per Default nicht aktiviert.)

    Und bei so einfachen Funktionen wie diesen kann Inlining verdammt viel bringen.

    Beispiel einer einfachen Funktion die einen Vektor um 90° dreht (2 Koordinaten vertauschen und eine davon invertieren): https://godbolt.org/z/Ehx58K8cG

    Ohne Inlining:

    rot90(Vector3D_ni):                  # @rot90(Vector3D_ni)
            push    rbx
            sub     rsp, 64
            movlps  qword ptr [rsp + 16], xmm0
            movss   dword ptr [rsp + 24], xmm1
            lea     rbx, [rsp + 16]
            mov     rdi, rbx
            call    Vector3D_ni::y() const
            movss   dword ptr [rsp + 12], xmm0      # 4-byte Spill
            mov     rdi, rbx
            call    Vector3D_ni::x() const
            xorps   xmm0, xmmword ptr [rip + .LCPI1_0]
            movaps  xmmword ptr [rsp + 48], xmm0    # 16-byte Spill
            mov     rdi, rbx
            call    Vector3D_ni::z() const
            movaps  xmm2, xmm0
            lea     rdi, [rsp + 32]
            movss   xmm0, dword ptr [rsp + 12]      # 4-byte Reload
            movaps  xmm1, xmmword ptr [rsp + 48]    # 16-byte Reload
            call    Vector3D_ni::Vector3D_ni(float, float, float) [complete object constructor]
            movsd   xmm0, qword ptr [rsp + 32]      # xmm0 = mem[0],zero
            movss   xmm1, dword ptr [rsp + 40]      # xmm1 = mem[0],zero,zero,zero
            add     rsp, 64
            pop     rbx
            ret
    

    Mit Inlining:

    rot90(Vector3D):                      # @rot90(Vector3D)
            movaps  xmm2, xmmword ptr [rip + .LCPI0_0] # xmm2 = [-0.0E+0,-0.0E+0,-0.0E+0,-0.0E+0]
            xorps   xmm2, xmm0
            shufps  xmm2, xmm0, 212                 # xmm2 = xmm2[0,1],xmm0[1,3]
            shufps  xmm2, xmm0, 82                  # xmm2 = xmm2[2,0],xmm0[1,1]
            movaps  xmm0, xmm2
            ret
    

    Selbst wenn du nicht x86 Assembler lesen kannst, solltest du sehen dass mit Inlining hier viel weniger gemacht werden muss als ohne Inlining. Und dabei enthält die Variante ohne Inlining noch nichtmal den Code der dann noch in den aufgerufenen Funktionen steht.



  • @hustbaer @SeppJ

    Danke euch für die Erklärung! Habe es besser verstanden als bei google oder sonst wo.
    Ich kann x86 / x64 Assembler lesen und sehe ja zudem noch den gravierenden Unterschied.



  • @hustbaer @SeppJ
    Also könnte ich doch quasi, wenn ich eine Klasse "Person" habe
    die Methoden wie beispielsweise getName() auch inline machen oder nicht?
    Diese Methode ist ja kurz und knackig.

    class Person{
    public:
     Person() = default;
     Person(std::string& name) : m_Name(name){}
    
    inline std::string Name(){
    return m_Name;
    }
    
    private:
    std::string m_Name;
    }
    


  • Mir ist gerade noch aufgefallen, wenn ich im header folgendes deklariere

    inline const float getX() const;
    

    und im source dann folgendes definiere

    const float Vector3D::getX() const{
    return m_Array[0];
    }
    

    und in der Main wie folgt benutzen möchte

    Vector3D vec3 = {42, 32, 22};
    std::cout << vec3.getX();
    

    bekomme ich ein unresolved external symbol .... "referenced in function main"

    Heißt das, dass ich inline functions direkt in der Header-File definieren sollte?


  • Mod

    Wie schon gesagt, Funktionen in der Klassendefinition sind sowieso inline:

    class Person{
    public:
     Person() = default;
     Person(std::string& name) : m_Name(name){}
    
      std::string Name(){
    return m_Name;
    }
    
    private:
    std::string m_Name;
    }
    

    Name() ist hier schon inline.

    inline ohne Code macht keinen Sinn. Das inline ist eine Eigenschaft des Codeblocks, es ist kein Teil der Signatur einer Klasse. Wenn du eine Funktion nur deklarieren möchtest, dann macht es überhaupt keinen Unterschied, ob sie später inline definiert ist oder nicht.

    void foo();
    
    // […]
    
    inline void foo()
    {
      // Code
    }
    

    Wieso man das bei einer Inlinefunktion je machen sollte, ist eine gute Frage. Vielleicht wenn man am Anfang eines Headers eine Übersichtssektion über alle Funktionen haben möchte, ohne Implementierungen dazwischen?



  • @SeppJ

    Ja genau. Es ist ja schöner, wenn man dort nur die Deklarationen hat und keine Implementierungen.
    Habs aber jetzt verstanden und schon umgeändert und meine Frage wurden super beantwortet.
    Vielen Dank!



  • @unkwnusr sagte in Eigene Vektorenklasse (Fragen, Verbesserungen):

    @hustbaer @SeppJ
    Also könnte ich doch quasi, wenn ich eine Klasse "Person" habe
    die Methoden wie beispielsweise getName() auch inline machen oder nicht?
    Diese Methode ist ja kurz und knackig.

    Da kann man sich schnell täuschen. Der C++ Code ist zwar bloss return m_Name;, aber der Asselblercode der da draus wird ist gar nicht "knackig": https://godbolt.org/z/1eae88nxK

    Also ja, kannst du. Bringt aber eher nix.



  • @SeppJ sagte in Eigene Vektorenklasse (Fragen, Verbesserungen):

    Das ist korrekt, dass dies die Absicht hinter der Existenz des Schlüsselwortes ist. Praktisch entscheidet das der Compiler aber selber schon ganz gut, und trifft seine eigene Entscheidungen, ganz unabhängig von dem inline Schlüsselwort.

    Ich denke es macht Sinn, beim inline-Schlüsselwort höchstens noch aus historischen Gründen dessen ursprüngliche Motivation zu erwähnen

    Dessen de facto einzige effektive Bedeutung ist heutzutage dein "anderer Nebeneffekt": "mehrere Definitionen sind erlaubt", was inline nicht zu einem Optimierungs- sondern zu einem Code- und Projektstruktur-Werkzeug macht. Gerade auch die etwas neueren inline-Variablen haben eigentlich gar nichts mehr mit dem ursprünglichen inlining zu tun. Das ist meines erachtens alles, was man als Anfänger zu wissen braucht. Auch die Größe der Funktion ist bei inline irrelevant. Die einzige Frage ist, ob der Aufbau des Projekts an dieser Stelle inline erfordert, z.B. weil selbst die 200-Zeilen-Funktion aus welchem Grunde auch immer im Header definiert sein soll (Header-Only Library z.B.).

    Will man Compiler-Inlining steuern, sollte man compiler-spezifische Werkzeuge wie __attribute__((always_inline)) oder __attribute__((noinline)) oder auf Build-Ebene LTO und spezielle Optimierungs-Flags verwenden. Bei Ersteren sollte man dann allerdings wirklich wissen was und warum man es tut. Hier wird die "Größe" der Inline-Funktion tatsächlich relevant.



  • @Finnegan Ich stimme dir generell diesbezüglich zu, aber ich habe auch mal in die CppCoreGuidelines geschaut und finde dort https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f5-if-a-function-is-very-small-and-time-critical-declare-it-inline:

    Over the last 40 years or so, we have been promised compilers that can inline better than humans without hints from humans. We are still waiting. Specifying inline (...) encourages the compiler to do a better job.

    🙂



  • @hustbaer sagte in Eigene Vektorenklasse (Fragen, Verbesserungen):

    Da kann man sich schnell täuschen. Der C++ Code ist zwar bloss return m_Name;, aber der Asselblercode der da draus wird ist gar nicht "knackig": https://godbolt.org/z/1eae88nxK

    Also ja, kannst du. Bringt aber eher nix.

    Da wird ja auch ein String-Kopie erzeugt, also

    std::string Name(){
      return std::string(m_Name);
    }
    

    Bei

    const std::string& Name() const {
      return m_Name;
    }
    

    sind es nur noch zwei Assembler-Mnemonics: geänderter Code

    Oder ohne Optimierung kompilieren.



  • @wob sagte in Eigene Vektorenklasse (Fragen, Verbesserungen):

    @Finnegan Ich stimme dir generell diesbezüglich zu, aber ich habe auch mal in die CppCoreGuidelines geschaut und finde dort https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f5-if-a-function-is-very-small-and-time-critical-declare-it-inline:

    Over the last 40 years or so, we have been promised compilers that can inline better than humans without hints from humans. We are still waiting. Specifying inline (...) encourages the compiler to do a better job.

    🙂

    Ja, ich muss zugeben, dass ich tatsächlich den Code der Compiler nicht im Detail kenne und weiss, ob die inline tatsächlich auch als "Hint" interpretieren. Wenn mir inlining oder nicht wichtig ist, verwende ich die compiler-spezifischen Attribute.

    In dem Sinne halte ich es aber für nicht gut, wenn inline zwei Bedeutungen hat. Vielleicht will man ja eine inline-Funktion, weil sie im Header ein soll, aber man möchte nicht, dass sie geinlined wird. Ich meine mich sogar vage erinnern zu können, schonmal irgendwo eine inline __attribute__((noinline)) f()-Funktion geschrieben zu haben 😉



  • @Th69
    Aus welchen Gründen sollte ich denn eine Methode beispielsweise so implementieren

    const std::string& getName() const{}
    

    Hier geht es sich darum, dass eine String-Referenz zurückgegeben wird.
    Liegt das daran, dass "std::string" benutzt wird? Oder gibt es da eine Regel so wie "pi*Daumen" 😃


  • Mod

    Wenn du mit Implementierung wirklich meinst, dass diese leer sein sollte (weil du {} am Ende schreibst): Nie.

    Wenn du meinst, warum man eine const-Referenz zurückgeben sollte: Es ist effizient. Faustregel: Alles was groß (viele Mal die Größe eines Pointers) oder dynamisch (wie hier der String) ist, ist teuer zu kopieren. Falls der Aufrufer unbedingt eine Kopie will, kann er sie immer noch selber machen.



  • @SeppJ
    Nein, die Funktion ist einfach leer für das Beispiel jetzt.
    Alles klar, das hilft mir weiter, danke.



  • Und umgekehrt solltest du bei deinen Getter-Funktionen auch direkt float zurückgeben.
    Und den Rückgabetyp bei isZero solltest du auch noch mal anpassen.


Anmelden zum Antworten