vollmann engineering gmbh

 
   engineering  training  presentations  publications  blog   
home
sitemap
C++: Implementation of Move Construction and Move Assignment  

 
 
 

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

21-Mar-2015

C++: Implementation of Move Construction and Move Assignment

Thomas Becker has a very good article on rvalue references and move semantics. Unfortunately, his article lacks a comprehensive discussion how to implement the move operations. This post tries to fill the gap.

Example

Assume a resource controlling wrapper class around a connection construct provided by a C library. We're interested here only in creating and closing such a connection, and here's the relevant part of the C header:

typedef int connection_id;
enum { BadConnection = -1 };

connection_id open_connection(char const *connect_str);
void close_connection(connection_id con);

A simple C++ class wrapper for it looks like this:

class Connection
{
public:
    Connection(std::string const &conStr)
        : con(open_connection(conStr.c_str()))
    {
        if (con == BadConnection)
        {
            throw std::runtime_error
                      ("Connection couldn't be created");
        }
    }

    ~Connection()
    {
        close_connection(con);
    }

    // no copy
    Connection(Connection const &) = delete;
    Connection &operator=(Connection const &) = delete;

    // access
    connection_id get() const
    {
        return con;
    }

private:
    connection_id con;
};

The constructor creates the connection, stores it and cares for errors. The destructor cleans up, and we provide an accessor function.
In our case it isn't useful to copy such connections, so we forbid it.

Move

While copying is not very useful for Connection, moving it makes sense. So we provide move construction and move assignment.
One thing to remember about moving is that it typically changes both objects, the moved-from and the moved-to object.

If you think about real things, this becomes obvious: if you move real matter from one table to the other, the original space is now empty (actually probably filled by air now).
For C++, if you move from oldObj to newObj, oldObj still stays around and is eventually destructed. You need to make sure that the destruction of oldObj doesn't cause any problems, e.g. by marking it invalid or empty.

In our example, if we move from one Connection con1 to con2, we must make sure that we don't call close_connection on the destruction of con1 (given that we didn't assign another value to it). For this, we use the special value BadConnection and check ib the destructor for it. To be able to re-use this code later, we also factor it out into a separate function:

    ~Connection()
    {
        closeCon(con);
    }

private:
    void closeCon(connection_id c)
    {
        if (c != BadConnection)
        {
            close_connection(c);
        }
    }

Jens Weller has some more notes on the state of the moved-from object on the Meeting C++ website.

Move Constructor

Given the notes above, the move constructor for our Connection is pretty straightforward:

    Connection(Connection &&other)
        : con(other.con)
    {
        other.con = BadConnection;
    }

One difference to the copy constructor is that we don't take the other object by const reference:

    Connection(Connection &&other)

As stated above, we need to modify the other object when moving.

In the member initializer list we take over the data from the other object. A more general version would be:

        : con(std::move(other.con))

This version also makes it more explicit that we want to move the data. However, as connection_id is a C type, there's no real difference between moving and copying.

And finally, in the body, we set the other object to an invalid state.

Move Assignment

Assignment is logically the combination of the destruction of the old value and construction of the new one. For move assignment, the construction is done by move construction. So here's the code that combines the actions of the destructor and the move constructor:

    Connection &operator=(Connection &&other)
    {
        if (&other == this)
        {
            return *this;
        }
        closeCon(con);
        con = other.con;
        other.con = BadConnection;
        return *this;
    }

Again, we don't take other by const reference because we need to modify rhs. Then we check for self assignment (more on this later).
After that, we do the actions of the destructor (closeCon(con)) and the move constructor (again, con = std::move(other.con) would be more general, but doesn't make a difference here) and finally return our own object.

This is now a perfectly valid implementation and probably as efficient as it gets. Maybe you heard at some time that this form of check against self assigment is not the preferred way to do it, but here (and for now) it's ok. I'll cover more of move assignment implementation options (including self-assignment, whether you should mark move operations as noexcept (yes, you should), and exceptions on move in general) in another blog post.


Comments

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

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