operator- (unary): pass-by-value oder pass-by-ref-to-const?



  • Hallo,

    ich versuche gerade etwas die "basics" aufzufrischen und hänge gleich schon wieder bei einer Frage:

    Welche der folgenden Versionen des unären Minus-Operators ist denn zu bevorzugen, aufgrund von besserer Performance o. Ä.? Meine Überlegungen waren, dass Version 1 evtl. besser bei temporären Objekten ist, bzw. meine ich gelesen zu haben: Wenn man eine Kopie anfertigen möchte, dann am Besten gleich bei der Parameterübergabe. Jetzt bin ich mir aber doch nicht sicher...

    struct Vec3 {  // kurz gehalten
        float x, y, z;
    };
    
    // Version 1
    Vec3 operator-(Vec3 vec)
    {
        vec.x = -vec.x;
        vec.y = -vec.y;
        vec.z = -vec.z;
        return vec;
    }
    
    // Version 2
    Vec3 operator-(const Vec3& vec)
    {
        return {-vec.x, -vec.y, -vec.z};
    }
    

    Würde mich über eine kurze Antwort sehr freuen.

    LG



  • Dein Lieblingscompiler macht aus beidem genau das gleiche.



  • Nimmt mich auch wunder. Bisher berücksichtige ich nur die Grösse, von der ich annehme, dass sie "übergeben" wird. https://ideone.com/t8R44O

    Zu temporären Objekten und "pass-by-ref-to-const" oder "entgegennahme als const reference" wurde geantwortet:

    @swordfish sagte in Frage zu verfügbarem Stack:

    A temporary bound to a reference parameter in a function call (§5.2.2 [expr.call]) persists until the completion of the full expression containing the call.

    Ob es noch Weiteres zu berücksichtigen gibt (und ob so alles stimmt), nimmt mich ebenfalls wunder.



  • Ob nun bei der Parameterübergabe oder bei der Rückgabe kopiert wird ist doch völlig wumpe.



  • @swordfish Kann ich jetzt so leider nicht bestätigen (gcc version 8.1.0 x86_64-posix-seh-rev0) mit g++ -O3 -S test.cpp.

    Mein verwendetes Testprogramm:

    #include <iostream>
    
    struct Vec3 {
        float x, y, z;
    
        Vec3(float x, float y, float z)
            : x{x}, y{y}, z{z}
        {
            std::cout << "constructor" << std::endl;
        }
    
        ~Vec3()
        {
            std::cout << "destructor" << std::endl;
        }
    
        Vec3(const Vec3& other)
        {
            std::cout << "copy constructor" << std::endl;
        }
    
        Vec3& operator=(const Vec3& other)
        {
            std::cout << "copy assignment" << std::endl;
            return *this;
        }
    
        Vec3(Vec3&& other)
        {
            std::cout << "move constructor" << std::endl;
        }
    
        Vec3& operator=(Vec3&& other)
        {
            std::cout << "move assignment" << std::endl;
            return *this;
        }
    };
    
    #define TOGGLE 0
    #if TOGGLE
    Vec3 operator-(const Vec3& vec)
    {
        return {-vec.x, -vec.y, -vec.z};
    }
    #else
    Vec3 operator-(Vec3 vec)
    {
        vec.x = -vec.x;
        vec.y = -vec.y;
        vec.z = -vec.z;
        return vec;
    }
    #endif
    
    int main()
    {
        Vec3 vec(0, 0, 0);
        Vec3 vec2 = -vec;
    
        Vec3 vec3 = -Vec3(0, 0, 0);
    }
    

    Nach erneuter Recherche glaube ich, dass bei Version 1 immer gemoved wird, und bei Version 2 kann RVO eingesetzt werden (was nicht bei Funktionsargumenten geht), deshalb ist Version 2 theoretisch besser als Version 1.

    Kann das jemand bestätigen oder habe ich noch einen Denkfehler drin?



  • Ja, ein riesen unterschied:

    foo(Vec3 const&):
            movss   xmm1, DWORD PTR [rsi+4]
            movss   xmm0, DWORD PTR [rsi+8]
            mov     rax, rdi
            movss   xmm3, DWORD PTR .LC0[rip]
            movss   xmm2, DWORD PTR [rsi]
            xorps   xmm1, xmm3
            xorps   xmm0, xmm3
            xorps   xmm2, xmm3
            movss   DWORD PTR [rdi+4], xmm1
            movss   DWORD PTR [rdi], xmm2
            movss   DWORD PTR [rdi+8], xmm0
            ret
    bar(Vec3):
            movss   xmm0, DWORD PTR [rsi]
            movss   xmm1, DWORD PTR .LC0[rip]
            mov     rax, rdi
            xorps   xmm0, xmm1
            movss   DWORD PTR [rsi], xmm0
            movss   xmm0, DWORD PTR [rsi+4]
            xorps   xmm0, xmm1
            movss   DWORD PTR [rsi+4], xmm0
            movss   xmm0, DWORD PTR [rsi+8]
            xorps   xmm0, xmm1
            movss   DWORD PTR [rsi+8], xmm0
            ret
    


  • Ich finde, es ist eine Frage der Schnittstelle. Intern wirds vermutlich auf den gleichen Code hinauslaufen. Als Schnittstelle würde ich aber meist "ref to const" bevorzugen. Jetzt nicht unbedingt in diesem speziellen Fall, aber ich seh auch nicht, was hier dagegen spricht.
    Ob irgendwo irgendwas kopiert wird, ist oft ein Implementierungsdetail. So ein Operator ist vielleicht nicht das beste Beispiel, aber stell dir vor, die Funktion wär einfach etwas länger, und mittendrin ist ein if, das aussteigt, ohne eine Kopie gemacht zu haben. Deswegen find ichs schon mal konsistenter, meist als const& zu übergeben, wenn nicht was anderes dagegen spricht.
    Ehrlich gesagt würds mich auch etwas stören, das anders zu machen. Allein schon, weil das für mich ein gewisses Signal darstellt, hier ist es besser, eine Kopie zu übergeben (die dann z.B. gemoved wird), und genau die Fälle will ich dann auch möglichst auf den ersten Blick erkennen.


Log in to reply