Konstante Getter-Funktionen?


  • Mod

    Ein move ist auch innerhalb der Funktionen beim return denkbar (in pipe sogar erforderlich) während die Kopie möglicherweise ausgelassen wird.
    Eigentlich:

    //return    call     return         call
        consumer(good());      //erwartet:    move      move
        consumer(bad());       //erwartet:    move      copy
        consumer(pipe(good()));//erwartet:    move      move     move (immer)   move
        consumer(pipe(bad())); //erwartet:    move      copy     move (immer)   move
    

    gcc 4.5.1 kennt die move-Regel beim lvalue-return noch nicht, gcc-4.6.3 macht das dann richtig:

    consume good
    consume bad
    move good consume good
    move bad consume bad
    


  • Klitzekleine Nachlässigkeit...

    ~X() {delete[] ptr;}
    

    Wenn man hier mitliest ist man ganz versessen darauf new/delete Fehler zu erspähen... 😉


  • Mod

    Wie schon ausgeführt, kann der obige Code nicht schlüssig demonstrieren, dass msvc konstante Objekte on modifizierbare rvalue-Referenzen bindet. Weil nicht klar wird, welche Copy/Move-Operationen ausgelassen werden. Eine kleine Modifikation ist erforderlich:

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    struct X {
      X(char const* str) : ptr(0) {ptr = new char[strlen(str)+1]; strcpy(ptr, str);}
      X(X&& other)      : ptr(other.ptr) {other.ptr = 0; cout << "move " << ptr << ' ';}
      X(X const& other) : ptr(0)         {ptr = new char[strlen(other.ptr)+1]; strcpy(ptr, other.ptr); cout << "copy " << ptr << ' ';}
      ~X() {delete []ptr;}
    
      char* ptr;
    };
    
    X good() { X x("good"); return *&x;}
    X const bad() { X x("bad"); return *&x;}
    
    X pipe(X x) { return *&x; }
    void consumer(X x) { std::cout << "consume " << x.ptr << '\n'; }
    
    int main()
    {
        // * = copy/move can be elided        return    call     return   call
        consumer(good());      //erwartet:    copy      move*
        consumer(bad());       //erwartet:    copy      copy*
        consumer(pipe(good()));//erwartet:    copy      move*    copy     move*
        consumer(pipe(bad())); //erwartet:    copy      copy*    copy     move*
    }
    

    Hier muss beim return in jedem Fall eine Copy gemacht werden, jeder zusätzliche Konstruktoraufruf würde also vom Funktionsaufruf stammen.
    gcc ist hier mal wieder zu aggresiv:

    consume good
    consume bad
    move good consume good
    move bad consume bad
    

    Hier wird also von gcc NRVO angewendet, obwohl das nicht zulässig ist. Also lieber so:

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    struct X {
      X(char const* str) : ptr(str) {}
      X(X&& other)      : ptr(other.ptr) {cout << "move " << ptr << ' ';}
      X(X const& other) : ptr(other.ptr) {cout << "copy " << ptr << ' ';}
      ~X() {}
    
      char* ptr;
    };
    
    X a("good"), b("bad");
    
    X good() { return a;}
    X const bad() { return b;}
    
    X pipe(X x) { return x.ptr==a.ptr?a:b; }
    void consumer(X x) { std::cout << "consume " << x.ptr << '\n'; }
    
    int main()
    {
        // * = copy/move can be elided        return    call     return   call
        consumer(good());      //erwartet:    copy      move*
        consumer(bad());       //erwartet:    copy      copy*
        consumer(pipe(good()));//erwartet:    copy      move*    copy     move*
        consumer(pipe(bad())); //erwartet:    copy      copy*    copy     move*
    }
    

    jetzt erhalte ich

    copy good consume good
    copy bad consume bad
    copy good copy good consume good
    copy bad copy bad consume bad
    


  • camper schrieb:

    X good() { X x("good"); return *&x;}
    X const bad() { X x("bad"); return *&x;}
     
    X pipe(X x) { return *&x; }
    

    Hier muss beim return in jedem Fall eine Copy gemacht werden, jeder zusätzliche Konstruktoraufruf würde also vom Funktionsaufruf stammen.
    gcc ist hier mal wieder zu aggresiv:

    MSVC:

    move good consume good
    move bad consume bad
    move good move good consume good
    move bad move bad consume bad
    

    camper schrieb:

    Also lieber so:

    #include <iostream>
    #include <cstring>
    using namespace std;
     
    struct X {
      X(char const* str) : ptr(str) {}
      X(X&& other)      : ptr(other.ptr) {cout << "move " << ptr << ' ';}
      X(X const& other) : ptr(other.ptr) {cout << "copy " << ptr << ' ';}
      ~X() {}
     
      char* ptr;
    };
    
    main.cpp(6): error C2440: 'initializing' : cannot convert from 'const char *' to 'char *'
    

    😉 Hatte das auch erst so versucht, dann doch aber mit strcpy & co gearbeitet. Mit der anderen Implementierung von X wie vorher und good()/bad()/pipe() wie bei dir bekomme ich das selbe: 1-2x copy, kein move.


  • Mod

    pumuckl schrieb:

    camper schrieb:

    X good() { X x("good"); return *&x;}
    X const bad() { X x("bad"); return *&x;}
     
    X pipe(X x) { return *&x; }
    

    Hier muss beim return in jedem Fall eine Copy gemacht werden, jeder zusätzliche Konstruktoraufruf würde also vom Funktionsaufruf stammen.
    gcc ist hier mal wieder zu aggresiv:

    MSVC:

    move good consume good
    move bad consume bad
    move good move good consume good
    move bad move bad consume bad
    

    Interessant.
    Also nochmal Faktencheck:

    n3337 12.8/31 schrieb:

    When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
    object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases,
    the implementation treats the source and target of the omitted copy/move operation as simply two different
    ways of referring to the same object, and the destruction of that object occurs at the later of the times
    when the two objects would have been destroyed without the optimization.123 This elision of copy/move
    operations, called copy elision, is permitted in the following circumstances (which may be combined to
    eliminate multiple copies):
    123) Because only one object is destroyed instead of two, and one copy/move constructor is not executed, there is still one
    object destroyed for each one constructed.
    — in a return statement in a function with a class return type, when the expression is the name of a
    non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified
    type as the function return type, the copy/move operation can be omitted by constructing
    the automatic object directly into the function’s return value
    — in a throw-expression, [...]
    — when a temporary class object that has not been bound to a reference (12.2) would be copied/moved
    to a class object with the same cv-unqualified type, the copy/move operation can be omitted by
    constructing the temporary object directly into the target of the omitted copy/move
    — when the exception-declaration of an exception handler (Clause 15) declares [...]

    in

    return *&x;
    

    liegt kein temporäres Objekt vor, also kommt der 3. Anstrich nicht in Frage.
    *&x ist auch nicht der Name eines lokalen Objektes, sondern schlicht ein anderer Ausdruck, der nur zufällig auf ein solches Objekt verweist. Der Standard gibt gcc also nicht das Recht, hier die Kopie auszulassen.

    n3337 12.8/32 schrieb:

    When the criteria for elision of a copy operation are met or would be met save for the fact that the source
    object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to
    select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload
    resolution fails, or if the type of the first parameter of the selected constructor is not an rvalue reference to
    the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an
    lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will
    occur. It determines the constructor to be called if elision is not performed, and the selected constructor
    must be accessible even if the call is elided. —end note ]

    Da schon geklärt wurde, dass Copy-Elision nicht ausgeführt werden kann, und der Grund dafür in der Form des Ausdrucks zu suchen ist und nicht etwa in der Tatsache, dass ein Funktionsargument zurückgegeben wird, kommt diese Optimierung auch nicht in Frage. Es muss kopiert werden.
    clang macht es übrigens richtig.

    Wie verhält sich msvc, bei der Variante mit globalen Objekten?

    pumuckl schrieb:

    main.cpp(6): error C2440: 'initializing' : cannot convert from 'const char *' to 'char *'
    

    Muss nat. const char* hin, hatte ich so getestet, nur dann hier beim Posten vergessen, zu ändern.



  • camper schrieb:

    Wie schon ausgeführt, kann der obige Code nicht schlüssig demonstrieren, dass msvc konstante Objekte on modifizierbare rvalue-Referenzen bindet. Weil nicht klar wird, welche Copy/Move-Operationen ausgelassen werden.

    Du hast (wie meistens) vollkommen Recht - und ich habe MSVC anscheinend zu unrecht beschuldigt. Und Clang vermutlich auch.

    camper schrieb:

    Eine kleine Modifikation ist erforderlich:

    (...)

    Hier muss beim return in jedem Fall eine Copy gemacht werden, jeder zusätzliche Konstruktoraufruf würde also vom Funktionsaufruf stammen.
    gcc ist hier mal wieder zu aggresiv:

    consume good
    consume bad
    move good consume good
    move bad consume bad
    

    Hier wird also von gcc NRVO angewendet, obwohl das nicht zulässig ist.

    Bekomm' ich mit MSVC das selbe Ergebnis.

    Ich hab meinen Test jetzt nochmal umgeschrieben:

    #include <iostream>
    #include <utility>
    
    using namespace std;
    
    struct foo {
    	explicit foo(char const* s) : m_str(s) { cout << "init " << m_str << "\n"; }
    	foo(foo const& other)  : m_str(other.m_str) { cout << "copy " << m_str << "\n"; }
    	foo(foo&& other) : m_str(other.m_str) { cout << "move " << m_str << "\n"; }
    	~foo() { cout << "destroy " << m_str << "\n"; }
    
    	foo& operator = (foo const& other) { m_str = other.m_str; cout << "copy-assign " << m_str << "\n"; return *this; }
    	foo& operator = (foo&& other) { m_str = other.m_str; cout << "move-assign " << m_str << "\n"; return *this; }
    
    private:
    	char const* m_str;
    };
    
    foo good() { return foo("good"); }
    foo const bad() { return foo("bad"); }
    
    void consume(foo) { }
    
    template <class T> void consume2(T&& t) {
    	consume(forward<T>(t));
    }
    
    int main()
    {
    	consume2(good());
    	cout << "\n";
    	consume2(bad());
    //	cout << "\n";
    //	good() = foo("nicht so gut");
    }
    

    Damit bekomme ich von MSVC 11...

    init good
    move good
    destroy good
    destroy good
    
    init bad
    copy bad
    destroy bad
    destroy bad
    

    ...was ich für vernünftig halte.

    (EDIT: GCC 4.5.1 macht das selbe: http://ideone.com/v4fkC )

    Ich schliesse daraus: Returntyp mit top-level const verhindert Zuweisung, aber auch (in manchen Fällen) move.

    Den Assignment-Operator auf Lvalues einzuschränken ist mMn. ganz klar die bessere Lösung.
    Einerseits muss man es nur 1x pro Klasse machen, und nicht pro Funktion die die Klasse als Returntyp verwendet. Und andrerseits verhindert es die unerwünschte Zuweisung, lässt uns aber das erwünschte move.

    Dummerweise kann MSVC 11 das noch nicht.

    BTW: gibt's ne Seite ala ideone wo man Clang 3.0, Clang 3.1, GCC 4.6 und/oder GCC 4.7 ausprobieren kann (inklusive Code Ausführen)?



  • camper schrieb:

    Wie verhält sich msvc, bei der Variante mit globalen Objekten?

    Die globalen Objekte, mit dem char const* member, ergeben folgendes:

    copy good consume good
    copy bad consume bad
    copy good copy good consume good
    copy bad copy bad consume bad
    

    Offenbar ist MSVC wie ich der Meinung dass es ohne wirkliche Ressourcen keinen Grund für moves gibt 😉 Das war der Grund, warum ich mit Allokation und strcpy etc gearbeitet hatte...



  • pumuckl schrieb:

    Offenbar ist MSVC wie ich der Meinung dass es ohne wirkliche Ressourcen keinen Grund für moves gibt 😉 Das war der Grund, warum ich mit Allokation und strcpy etc gearbeitet hatte...

    Glaub' ich nicht.
    Beobachtbare Seiteneffekte sind beobachtbare Seiteneffekte - ob das jetzt die Ausgabe von Strings oder new/delete sind muss egal sein.


  • Mod

    pumuckl schrieb:

    copy good consume good
    copy bad consume bad
    copy good copy good consume good
    copy bad copy bad consume bad
    

    Offenbar ist MSVC wie ich der Meinung dass es ohne wirkliche Ressourcen keinen Grund für moves gibt 😉 Das war der Grund, warum ich mit Allokation und strcpy etc gearbeitet hatte...

    Das zeigt eigentlich nur, dass der Compiler RVO immer einsetzt, wo es möglich ist - das ist zu erwarten, denn diese Optimierung ist einfacher (sie stellt keine zusätzlichen technischen Bedingungen an den Kontext), und Compiler machen das schon seit 10 Jahren zuverlässig.


Anmelden zum Antworten