![]() |
engineering training presentations publications blog | ||||||||||||||||||||
home sitemap |
C++: More on Implementing Move Assignment | |||||||||||||||||||
c++ embedded c++ embedded linux
|
15-Apr-2015 C++: More on Implementing Move AssignmentIn my previous post I ended up with this example for implementing move assignment: class Connection { public: Connection(std::string const &conStr); Connection(Connection &&other); ~Connection(); Connection &operator=(Connection &&other) { if (&other == this) { return *this; } closeCon(con); con = other.con; other.con = BadConnection; return *this; } private: void closeCon(connection_id c) { if (c != BadConnection) { close_connection(c); } } connection_id con; };
This is a perfectly valid implementation. Swapping
Assume for a moment that our Connection &operator=(Connection other) { std::swap(con, other.con); return *this; }
This is the copy/swap idiom presented in Dave Abrahams' series on
rvalue references (the original entry is not reachable anymore,
but The Internet Archive has a
copy). Before we look at that implementation in detail, we first look at a variant of that one. Some programmers take this idiom and apply it as is to move assignment: Connection &operator=(Connection &&other) { std::swap(con, other.con); return *this; } This form was discussed at length on Scott Meyers' blog. Scott complained about two different aspects of this form: performance and delay of destruction. Delay of Destruction
I believe the delay is more important. Connection con1{"SomeConString"}; Connection con2{"OtherConString"}; con1 = std::move(con2);
At this point, you probably assume that To make sure the old value is destructed timely, you can add a temporary: Connection &operator=(Connection &&other) { Connection tmp(std::move(other)); std::swap(con, tmp.con); return *this; }
This code is equivalent to the unified implementation above, where
PerformanceSo let's now look at the performance argument. If you expand this implementation to basic operations, you get: Connection &operator=(Connection &&other) { // Connection tmp(std::move(other)); connection_id tmp_con = other.con; other.con = BadConnection; // std::swap(con, tmp.con); connection_id tmp2_con = con; con = tmp_con; tmp_con = tmp2_con; // destruction of tmp if (tmp_con != BadConnection) { close_connection(tmp_con); } return *this; } If you compare that against our original version, we have here
Though the conditional branch could be expensive, I'd say that our
original version is still better, but not by much.
Our One last remark on performance. Howard Hinnant (in the discussion on Scott's blog, in his ACCU 2014 keynote, and nearly always when you talk to him) puts a great emphasis on performance. He's right – for his job. He's implementing libraries for general or performance critical applications. And he correctly states that the original goal of move semantics was performance. But move semantics is also very useful for move-only types, and such types (like file streams, communication channels, windows, ...) are often heavyweight types and a few additional int assigments on moves are in such cases probably not measurable. So, as usual, it really depends on the specific types you're dealing with. Destruction + ConstructionOur handcrafted version of move assignment is nothing else than the self-assignment check and the manual inlining of the destructor and the move constructor. Instead of doing that manually, we could just call them: Connection &operator=(Connection &&other) { if (&other == this) { return *this; } this->~Connection(); new (this) Connection(std::move(other)); return *this; } I personally like this version very much, as it shows exactly what the semantics of move assignment are and it avoids code duplication. And it's exactly as performant as the handcrafted version (in this case).
Unfortunately, this version has a problem with polymorphism.
Not only that there's some overhead if there is a virtual function table,
but you also get the wrong virtual function table if you derive from
Using the destructor and move constructor directly should be no problem
if you declare your class as SummaryUsing the swap idiom is not perfectly performant, but is generally not overly expensive and correct. Using destructor and move constructor directly is generally performant, but is not generally correct. But there's still more to tell about move assignment. More on self-assignment and exceptions in another blog post. CommentsAny comments, remarks, correction, etc. are welcome. |
Moving Variants |
||||||||||||||||||
home
sitemap
engineering
consulting
coaching
training
presentations
publications
blog
contact
copyright © 2003-2023 vollmann engineering gmbh |