Code-Duplication durch RValue-Referenzen // perfect forwarding ohne templates



  • Ich stelle grade fest, dass ich zur Unterstützungvon eventuellen Move-Operationen häufiger mal Code duplizieren muss. Bisher kennt man ein ähnliches Problem ja schon, wenn man von einer Funktion eine const und eine non-const Version anbieten wollte, bei rvalues scheint sich das Problem zu wiederholen:

    //erklärt sich von selbst...
    void InterfaceEventQueue::pushEvent(InterfaceEvent const& evt)
    {
      boost::mutex::scoped_lock lock(the_mutex);
      evtQueue.push(evt);
      lock.unlock();
      the_condition_variable.notify_one();
    }
    

    Okay, nun ist InterfaceEvent movable, also (fast) das selbe nochmal:

    void InterfaceEventQueue::pushEvent(InterfaceEvent&& evt)
    {
      boost::mutex::scoped_lock lock(the_mutex);
      evtQueue.push(std::move(evt));
      lock.unlock();
      the_condition_variable.notify_one();
    }
    

    Wie löst ihr sowas? Mit einem Template wäre das nicht passiert:

    template <class IEvt>
    void InterfaceEventQueue::pushEvent(IEvt&& evt)
    {
      boost::mutex::scoped_lock lock(the_mutex);
      evtQueue.push(std::forward<IEvt>(evt));
      lock.unlock();
      the_condition_variable.notify_one();
    }
    

    Der Nachteil ist, dass die Kapselung flöten geht und ich im Allgemeinen ggf. noch static_asserts einbauen muss, um Funktionen nicht falsch anzuwenden.



  • Ich lasse einfach einen InterfaceEvent evt übergeben. Wenn der Aufrufer jetzt std::move nutzt, hat man 2x move. Wenn er es nicht nutzt, 1x Copy, 1x move. Ich denke da kommt es kaum zu Performanceunterschieden zu 1xCopy.

    Edit: http://ideone.com/E7aPH



  • cooky451 schrieb:

    Ich lasse einfach einen InterfaceEvent evt übergeben.

    Wie meinst du? InterfaceEventQueue::pushEvent(InterfaceEvent evt) ? Dann kompierst du doch auf jeden Fall? InterfaceEventQueue::pushEvent(InterfaceEvent&& evt) funktioniert nicht mit konstanten lvalues.

    Wenn der Aufrufer jetzt std::move nutzt, hat man 2x move. Wenn er es nicht nutzt, 1x Copy, 1x move.

    Der Aufrufer hat nicht immer was zu moven.

    Ich denke da kommt es kaum zu Performanceunterschieden zu 1xCopy.

    Möp. Genau das ist doch der Grund für rvalue-Referenzen und moving, nämlich dass Kopien unnötig teuer sind. Wenn mir das egal wäre, hätte ich auf den ganzen rvalue-Kram von Anfang an verzichtet.



  • Kopierst du im ersten Code in Zeile 5 nicht eh?

    Edit:
    Ich verstehe somit deine Kritik nicht so ganz. Wenn der Aufrufer nicht moven kann, musst du doch eh kopieren. Dann steht also move + copy vs. copy. Wenn der Aufrufer moven kann, hast du 2x move, also das, was angestrebt wird.



  • Wie wärs mit:

    void InterfaceEventQueue::pushEvent(InterfaceEvent const& evt)
    {
        pushEvent(InterfaceEvent(evt));
    }
    


  • @PI
    Sieht cool aus, aber wie vermeidet das eine Kopie?



  • Da wird 1 mal kopiert und 1 mal gemoved.



  • 314159265358979 schrieb:

    Da wird 1 mal kopiert und 1 mal gemoved.

    Ich würde eher sagen damit wird einmal kopiert und nichts gemoved. Aber abgesehen davon war das Ziel doch, die Möglichkeit zu haben nicht zu kopieren?



  • Hier wird ein temporärer Wert erzeugt, mit diesem wird dann die move-Version aufgerufen, die den Wert in die Queue "hineinmoved". Eine Kopie wird sich nicht vermeiden lassen, dazu ist die Copy-Version schließlich da.

    Edit: Ach ne, vergesst das wieder. Kommt aufs selbe raus wie cooky's Version, nur mit expliziter Kopie.



  • Hä? Habe ich das Problem irgendwie nicht verstanden? Ich formulier's mal so, wie ich es verstanden habe:
    Es soll zwei möglichkeiten geben, etwas über eine Funktion in die nächste zu geben: Kopieren oder Moven.
    Jetzt hat man aber quasi zwei mal den Code, weil man je eine Funktion zum Moven und eine zum Kopieren braucht. Lösung:
    Man erwartet gleich einen Value weil man davon ausgeht, dass "move" quasi gratis ist. Jetzt kann der Aufrufer sich entscheiden, ob er das übergebene Objekt noch braucht, oder nicht.
    Wie passt da dein Vorschlag mit der const& rein, ich raffs nicht? Da brauchst du dann doch immer noch eine zweite Funktion zum Moven?



  • pumuckl schrieb:

    Wie meinst du? InterfaceEventQueue::pushEvent(InterfaceEvent evt) ? Dann kompierst du doch auf jeden Fall?

    Nein, wenn InterfaceEvent movable ist, kann bei RValues der Move-Konstruktor angewandt werden. Und LValues machst du mit std::move() zu RValues. Und konstante LValues kopierst du...



  • Hm...

    void pushEvent(InterfaceEvent&& e)
    {
        pushEventImpl(std::move(e));
    }
    
    void pushEvent(InterfaceEvent const& e)
    {
        pushEventImpl(e);
    }
    
    template <typename T>
    void pushEventImpl(T&& arg)
    {
        boost::mutex::scoped_lock lock(the_mutex);
        evtQueue.emplace(std::forward<T>(arg));
        lock.unlock();
        the_condition_variable.notify_one(); 
    }
    

    Kosten bei Copy-Version: 1 Kopie
    Kosten bei Move-Version: 1 Move



  • Nexus schrieb:

    pumuckl schrieb:

    Wie meinst du? InterfaceEventQueue::pushEvent(InterfaceEvent evt) ? Dann kompierst du doch auf jeden Fall?

    Nein, wenn InterfaceEvent movable ist, kann bei RValues der Move-Konstruktor angewandt werden. Und LValues machst du mit std::move() zu RValues. Und konstante LValues kopierst du...

    Stimmt auffallend, danke euch. Ich habe dann allerdings in allen Fällen den Aufwand des Move-Ctors gegenüber der Alternative mit den zwei Überladungen by R-Ref/const-L-Ref. Wobei der eine move-ctor wieder wegoptimiert werden kann.

    Das pauschale "take arguments by const ref" von früher zählt also nicht mehr für moveable Klassen, wenn die per copy/move weitergereicht werden.



  • Wie gross ist dein InterfaceEvent in Byte?



  • Erscheint es euch eigentlich auch etwas doof, dass man "echtes" perfect Forwarding nur durch diese komische Sonderregel mit Templates bekommt? Was soll das?



  • knivil schrieb:

    Wie gross ist dein InterfaceEvent in Byte?

    Ein enum und ein unique_ptr, also irgendwas um die 16 byte auf x64. Mir gehts nur um den allgemeinen Fall, wo man auch mal mit größeren Movables zu tun hat.



  • pumuckl schrieb:

    Das pauschale "take arguments by const ref" von früher zählt also nicht mehr für moveable Klassen, wenn die per copy/move weitergereicht werden.

    Mein Reden!

    Wenn man vom Typ weiß, dass er einen effizienten move-ctor hat, darf man in dieser Situation wieder pass-by-value verwenden. Im Idealfall kann der Compiler hier auch ein unnötiges Move wegoptimieren (move elision), so dass es keinen Laufzeitunterschied macht. Das klappt zwar nicht immer, aber so schlimm wäre es ohne move elision auch nicht.

    class person {
    public:
      explicit person(string name) : name_(move(name)) {}
    private:
      string name_;
    };
    

    Bei generischem Code sieht das aber wieder etwas anders aus, siehe z.B. vector<>::push_back. Dort findet wieder die Überladung mit &&/const& statt. Allerdings kann man dort versuchen, wenigstens den Schreibaufwand zu reduzieren:

    template<class T, class Alloc>
    class vector v {
      ::
      void push_back(T const& x) {emplace_back(     x );}
      void push_back(T     && x) {emplace_back(move(x));}
    
      template<class...Args>
      void emplace_back(Args&&...args) {...}
      ::
    };
    

Anmelden zum Antworten