vollmann engineering gmbh

 
   engineering  training  presentations  publications  blog   
home
sitemap
C++: More on Implementing Move Assignment  

 
 
 

design
c++
embedded c++
embedded linux
 
 
 
                  
vollmann engineering
publications
presentations
blog
person

15-Apr-2015

C++: More on Implementing Move Assignment

In 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.
But it's not the only way to implement move assignment.

Swapping

Assume for a moment that our Connection class is not move-only, but also copyable (with a reasonable copy constructor).
Then this would be a valid implementation of assignment:

    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).
This is an implementation of move assignment and copy assignment at the same time: depending on the actual call the compiler will either use the copy constructor to intialize other if the initializer at the call site is an lvalue, or it will use the move constructor, if the initializer is an rvalue. And even if you don't have a copy constructor (as for our Connection), the above version is still a valid implementation for move assignment only. The compiler will only complain if you try to assign an lvalue, but you get such a complaint (though a different one) anyway for a move-only class.

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.
Look at this usage:

    Connection con1{"SomeConString"};
    Connection con2{"OtherConString"};
    con1 = std::move(con2);

At this point, you probably assume that con1 was closed, as it was overwritten by con2. But if you use the above swap-based implementation, this is not true. The connection to "SomeConString" is only closed after con2 goes out of scope, which may be much later than required by the program logic. If other in the move assignment function would be a real rvalue, the delay in destruction would be small enough to probably never cause problems. But given std::move, I consider the delay to be unacceptable.

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 other was passed by value, but is more explicit about move-only (and will produce slightly better compiler errors if copy assignment is tried). And it solves the destruction-delay problem, as after the swap tmp contains the the old value of *this, which gets destructed when the function returns.

Performance

So 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

  • two additional local (probably int) variables with initialization
  • one more (probably int) assignment
  • one less pointer comparison with conditional branch

Though the conditional branch could be expensive, I'd say that our original version is still better, but not by much. Our Connection has just one enum member. If you have members that are more expensive to move, the handcrafted version gains more against the swap version, how much really depends on the members of your class. On the other hand the swap version is a simple idiom to follow, while handcrafted versions always require more thought. So I won't condemn you if you generally prefer the swap version.

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 + Construction

Our 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 Connection, but don't override the move assignment. If you don't have virtual functions, having derived classes will probably work, but is still undefined behaviour as you

  • call the wrong destructor
  • (re-)construct the object as a different type than you use it

Using the destructor and move constructor directly should be no problem if you declare your class as final, and if your class doesn't inherit from other classes it's perfectly performant.

Summary

Using 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.


Comments

Any comments, remarks, correction, etc. are welcome.

Moving Variants  
  home sitemap engineering consulting coaching training presentations publications blog contact
copyright © 2003-2023 vollmann engineering gmbh