RVO und günstige Konvertierung



  • Hallo, ich habe zwei Klassen MyClass und Convertible , wobei Convertible konvertierbar zu MyClass ist. Nun ist es so, dass Objekte von Convertible immer temporär sind, denn ich habe eine Funktion

    Convertible Create();
    

    wobei die zurückgegebenen Objekte immer in MyClass -Objekten gespeichert werden. Es wird also nie ein Convertible direkt abgespeichert (dass nicht direkt MyClass zurückgegeben wird, ist nur ein Implementierungsdetail).

    Nun habe ich mir überlegt, wie ich die Konvertierung möglichst ohne unnötige Kopien implementieren kann. RValue-Referenzen wären hier ideal, doch ich muss das Ganze mit C++98-Mitteln und TR1 lösen (also auch kein Boost). Beide Klassen haben den gleichen Member, der relativ teuer zu kopieren ist. Ich hätte die Konvertierung spontan so implementiert:

    MyClass::MyClass(Convertible c)
    {
        myMember.swap(c.member);
    }
    

    Ist das im Hinblick auf RVO klug, oder könnte man es besser machen? Da ja Convertible -Objekte immer temporär sind, könnte man vielleicht auch etwas tricksen.

    MyClass::MyClass(const Convertible& c)
    {
        myMember.swap( const_cast<Convertible&>(c).member );
    }
    

    Würdet ihr sowas "Unsauberes" in Kauf nehmen? Oder sollte ich mir was Komplexeres überlegen wie den Trick von std::auto_ptr und std::auto_ptr_ref ?


  • Mod

    Nexus schrieb:

    MyClass::MyClass(Convertible c)
    {
        myMember.swap(c.member);
    }
    

    Das.



  • Dann sind moderne Compiler also in der Lage, die Kopie bei dem Code wegzuoptimieren. Vielen Dank für die Antwort!



  • Falls das teuer zu kopierende Ding nicht modifiziert wird, dann könntest du shared_ptr verwenden.
    Bzw. da du keine Boost verwenden willst/kannst, könntest du Reference-Counting verwenden.
    Etwas ala boost::intrusive_ptr ist schliesslich bloss ein paar Zeilen Code, das ist schnell selbst programmiert (oder aus den Boost Headern rauskopiert und soweit angepasst dass es ohne den Rest der Boost läuft).

    Oder, falls das teuer zu kopierende Ding doch modifiziert werden soll, das Programm aber strikt single-threaded ist, dann wäre COW eine Option.

    Zahlt sich natürlich alles nur aus, wenn du auch Fälle hast die nicht von der RVO abgedeckt werden.



  • Ja, aber mit shared_ptr (was sogar im TR1 wäre) hätte ich wieder zwei Freestore-Allokationen und anschliessend eine dauernde Indirektion. Ich weiss, das sollte nicht allzu schlimm sein, aber mich interessiert halt eine möglichst schnelle Lösung.

    MSVC++ und g++ scheinen hier die Kopie wegzuoptimieren, das ist schon mal sehr gut. C++0x-kompatible Compiler werden ausserdem implizite Move-Konstruktoren generieren, falls ich Kopierkonstruktor etc. nicht definiere.



  • Wie kann man da die Kopie wegoptimieren? Wenn beim ersten Code keine Kopie erstellt wird, wird ja das Objekt das übergeben wird geändert, was doch garnicht sein darf. 😕

    Der zweite Code wird ja immer das übergebene Objekt ändern.


  • Mod

    ??????? schrieb:

    Wenn beim ersten Code keine Kopie erstellt wird, wird ja das Objekt das übergeben wird geändert, was doch garnicht sein darf.

    Das ist nur dann ein Problem, wenn man nach dem Funktionsaufgriff noch auf das Argument zugreifen kann, weil es sich z.B. um ein benanntes Objekt handelt. In diesem Fall kann die Kopie auch nicht vermieden werden.

    foo source() { return foo(); }
    void sink(foo);
    
    ...
    sink(source());//(1)
    

    Hier (1) wird das foo-Objekt maximal zwei mal kopiert, einmal in source selbst, ein zweites mal beim Aufruf von sink. In beiden Fällen darf der Compiler die Kopie aber auslassen.



  • @Nexus:
    Was du schreibst ist schon richtig, war ja nur ein Vorschlag.
    Ich kann nicht wissen welche Variante bei deinem Problem besser ist: dass man die Kopie immer verhindert, dafür alles eine Spur langsamer wird, oder dass der "keine Kopie" Fall ne Spur schneller wird, dafür manchmal doch wieder kopiert werden muss.



  • Ach, mit "immer temporär" meint er, dass er z.B. nur sowas macht "new MyClass(Create())".



  • hustbaer schrieb:

    @Nexus:
    Was du schreibst ist schon richtig, war ja nur ein Vorschlag.
    Ich kann nicht wissen welche Variante bei deinem Problem besser ist: dass man die Kopie immer verhindert, dafür alles eine Spur langsamer wird, oder dass der "keine Kopie" Fall ne Spur schneller wird, dafür manchmal doch wieder kopiert werden muss.

    Ja, stimmt schon. Mir ist bewusst, dass man mit RVO immer das Risiko eingeht, dass eben doch Kopien entstehen, die alles langsam machen. Jedoch schätze ich den Anteil der Benutzer mit moderneren Compiler eher hoch ein (und mir gefällt der Gedanke irgendwie nicht, diese zu "bestrafen").

    Der Performanceverlust wäre in meinem Fall wahrscheinlich auch verkraftbar (dennoch habe ich die Frage gestellt, um generell RVO und gängige Praxis etwas besser kennenzulernen). Jedenfalls vielen Dank für die Hinweise!

    ??????? schrieb:

    Ach, mit "immer temporär" meint er, dass er z.B. nur sowas macht "new MyClass(Create())".

    Genau, daher auch meine unschöne Idee mit const_cast . Wie ist das dort eigentlich, wäre das undefiniertes Verhalten? Oder wäre das nur der Fall, wenn man const von einem benannten, const -qualifizierten Objekt wegcastet?


  • Administrator

    Nexus schrieb:

    Der Performanceverlust wäre in meinem Fall wahrscheinlich auch verkraftbar (dennoch habe ich die Frage gestellt, um generell RVO und gängige Praxis etwas besser kennenzulernen). Jedenfalls vielen Dank für die Hinweise!

    Hmmm, dürfte die gängige Praxis in dem Fall nicht eher sein, dass man beide Versionen testet, mit einem Profiler analysiert und sich dann für die beste Option für den/die eingesetzen Kompiler entscheidet? 🙂

    Nexus schrieb:

    Wie ist das dort eigentlich, wäre das undefiniertes Verhalten? Oder wäre das nur der Fall, wenn man const von einem benannten, const -qualifizierten Objekt wegcastet?

    Oder anders ausgedrückt: Wer hat Lust den Standard hervorzuholen und zu blättern? 😃

    Grüssli



  • Dravere schrieb:

    Hmmm, dürfte die gängige Praxis in dem Fall nicht eher sein [...]

    Nein.

    Da auf MSVC++ und g++ die Kopie wegoptimiert wird, ist es sicher schneller als mit shared_ptr . Um das zu wissen, muss ich keinen Profiler bemühen.

    Abgesehen davon deutet mein "generell" darauf hin, dass es nicht nur um den speziellen Fall hier geht.


  • Administrator

    Nexus schrieb:

    Da auf MSVC++ und g++ die Kopie wegoptimiert wird, ist es sicher schneller als mit shared_ptr . Um das zu wissen, muss ich keinen Profiler bemühen.

    Unter Profiler habe ich jetzt nicht unbedingt nur Zeitmessung verstanden, sondern eine ganz allgemeine Codeanalyse, wodurch auch Assambleranalysen dazugehören.

    Nexus schrieb:

    Abgesehen davon deutet mein "generell" darauf hin, dass es nicht nur um den speziellen Fall hier geht.

    Naja, meine Antwort war auch generell gemeint. Bei jedem Problem wird man sich zuerst keine grösseren Gedanken machen und danach schauen, was schneller ist.

    Ich persönlich würde zuerst einfach auf RVO setzen und danach schauen, ob es Probleme macht. Grund dafür ist, dass die RVO Lösung die einfachste und sauberste ist. Man kann sie einfach warten und setzt noch keinerlei Tricks ein. Tricks sollte man erst einsetzen, wenn man anfängt zu optimieren. Zumindest ist das eben meine Meinung 🙂

    Grüssli



  • Dravere schrieb:

    Bei jedem Problem wird man sich zuerst keine grösseren Gedanken machen und danach schauen, was schneller ist.

    Das kann man so nicht stehen lassen. Es schadet nie, sich zum Laufzeitverhalten im Voraus Überlegungen anzustellen, gerade wenn es sich nicht um Mikrooptimierungen handelt. Aber wir müssen die Premature-Optimization-Diskussion nicht zum x-ten Mal führen, wir sind uns grösstenteils eh einig 🙂

    Dravere schrieb:

    Ich persönlich würde zuerst einfach auf RVO setzen und danach schauen, ob es Probleme macht. Grund dafür ist, dass die RVO Lösung die einfachste und sauberste ist. Man kann sie einfach warten und setzt noch keinerlei Tricks ein.

    Ah, du meinst im Bezug auf const_cast . Ja, das habe ich auch vor. Dennoch hätte mich interessiert, ob hier ein const_cast überhaupt erlaubt wäre. Der Standard drückt sich nämlich nicht ganz klar aus (denn das temporäre Objekt ist ja nicht konstant):

    §7.1.5.1/4 The cv-qualifiers schrieb:

    Except that any class member declared mutable can be modified, any attempt to modify a const object during its lifetime results in undefined behavior.


  • Administrator

    Nexus schrieb:

    Dennoch hätte mich interessiert, ob hier ein const_cast überhaupt erlaubt wäre. Der Standard drückt sich nämlich nicht ganz klar aus (denn das temporäre Objekt ist ja nicht konstant):

    §7.1.5.1/4 The cv-qualifiers schrieb:

    Except that any class member declared mutable can be modified, any attempt to modify a const object during its lifetime results in undefined behavior.

    Ich würde in einem anderen Abschnitt suchen: 12.2 Temporary objects, Abschnitt 5
    Da geht es darum, dass die Lebensdauer von temporären Objekten verlängert wird, wenn sie an eine Referenz auf ein konstantes Objekt gebunden werden. Sicher bin ich mir nicht, aber womöglich könnte es funktionieren, da die Lebensdauer des temporären Objektes bis zum Ende des Konstruktors von MyClass verlängert wird. Den ganzen Text in diesem Abschnitt will ich jetzt hier nicht reinkopieren. Kann sich jeder selber anschauen, welcher einen C++ Standard zuhause rumliegen hat. 🙂

    Grüssli



  • Es geht ja mehr um die const -Qualifizierung als die Lebensdauer. Dass ein Const-Referenz-Parameter temporäre Objekte am Leben hält, ist mir bewusst.

    Von mir aus gesehen müsste der const_cast prinzipiell funktionieren (was nicht heisst, dass ich ihn hier einsetzen werde).



  • Wenn du das Convertible wirklich nur in dem einen Zusammenhang verwendest, was spräche dann dagegen:

    class Convertible
    {
      mutable CopyExpensive member;
    };
    
    //[...]
    
    MyClass::MyClass(Convertible const& c)
    {
      member.swap(c.member); //kein böser const_cast nötig ;)
      //...
    }
    

  • Administrator

    Nexus schrieb:

    Es geht ja mehr um die const -Qualifizierung als die Lebensdauer. Dass ein Const-Referenz-Parameter temporäre Objekte am Leben hält, ist mir bewusst.

    Ich würde schon sagen, dass es um die Lebensdauer geht. Schlussendlich geht es nämlich darum, auf welches Objekt die Referenz verweist und ob dieses Objekt konstant ist oder nicht. Wenn du daher ein nicht konstantes Objekt zurückgibst und die Lebensdauer dieses temporären Objektes verlängerst bis nach dem Ausführen des Konstruktors, dann verweist die Referenz auf ein tatsächlich nicht konstantes Objekt. Das war zumindest meine Überlegung, ob die stimmt ist eine andere Frage 🤡

    Grüssli



  • Okay. Danke auch für deinen Vorschlag, pumuckl. Dass ich da nicht drauf gekommen bin... 😉


Anmelden zum Antworten