Wednesday, December 5, 2012

Move Semantics and ctors and assignment


Move Semantics

OK, so what are they? Consider this example.

const BigThing bigThing = makeBigThing();

Anyone worried about performance is going to cringe. Look at the wasted temporaries. We create data on the heap in the function, data in the intermediate temporary, and finally data again in the bigThing.So we do this instead.

BigThing bigThing;
makeBigThing(bigThing);

But that's not what we want at all, and it's harder to understand. Of course we could do this all with pointers, but that causes huge problems and syntax often looks weird, especially when using operators.

C++ now has move semantics, and perfect forwarding. This is a perfect example of why it was created. What do we know about those temporary objects? Well, they're temporary. So why not steal their guts? Turn them into organ donors. That's what move semantics does. Today I'll explain one of the ways we can use move semantics using rValue references. I'll show how we can take advantage of them to make a highly efficient assignment operator.

So how do we write them? What do they look like? What is an rValue anyway?

Let's go through these and others with a very common example. Let's do an rValue reference copy constructor. We will assume a class Foo that holds a class Bar by pointer and we want everything to be deep copies. Here is our class Bar:

class Bar
{
    SomeBigThing m_Big;
public:
    Bar() = default;
    Bar(const& Bar) = default;
    ~Bar() = default;
    Bar& operator=(const Bar&) = default;
};

We're using the default keyword here. It may seem a bit strange to say you are using the functions that are created for you anyway, but on the other hand, isn't it nice that you now know what I intend that class to do? It's an efficient way to tell everyone you want those default functions. delete removes the function so you can easily prevent copying.

So now we get to the rValue copy constructor.

class Foo
{
    Bar* m_Bar;
public:
    Foo(Foo&& rhs) : m_bar(rhs.m_bar) { rhs.m_bar = nullptr; }
};

So what the hell is Foo&& ?????
That is the rValue reference. OK. It looks a bit weird, but it's not too bad. The real question is what is an rValue. The full, 100% correct answer is a bit tedious, but fortunately the concept is easy enough to grasp with a few examples and applying to questions to an object. First some examples.

    int i; // lValue
    int j = i; // lValue
    int k = someFunc(); // k is an lValue, 
                        // the return value of someFunc is an rValue
    42; // rValue
    4 * 5; // rValue

rValues are the temporary, unnamed objects we are trying to get rid of. That leads to a very easy way of thinking about rValues. They have two properties that are simple to test for.
  • They do not have a name.
  • You cannot get their address.
Simple.

OK? There is one slight hitch, and I promise I'm not trying to make this difficult. The parameter coming in to Foo(Foo&&) is an rValue, but... rhs is an lValue. How do we know this? It has a name and you can get its address. But it's OK. We know it came in as an rValue so we can use it as an organ donor. 

So let's look at that code again.

class Foo
{
    Bar* m_Bar;
public:
    Foo(Foo&& rhs) : m_bar(rhs.m_bar) { rhs.m_bar = nullptr; }
};


What's going on here? Well, we simply make a shallow copy of the pointer and set rhs.m_Bar to null. This is important! The destructor WILL be called on rhs and if m_Bar is not set to null, it will be deleted, destroying the whole point of doing the rValue copy in the first place.

In the last post, I talked about the assignment operator. I also wrote that that old paradigm was now different because of rValue references and move semantics. If you remember we created a temporary and then immediately deleted it. What a waste, but in the past there really was no way around it. But what is

    Foo(rhs); 

It's an rValue! And what do we know about rValues? They are temporary and you can steal from them. So our assignment operator becomes this:

    Foo& operator=(Foo rhs)
    {
        *this = std::move(rhs);
        return *this;
    }

Now all that's left is is to write the rValue ref version of the assignment operator.

    Foo& operator=(Foo&& rhs)
    {
        delete m_bar;
        m_bar = rhs.m_bar;
        rhs.m_bar = nullptr;
        return *this;
    }

We know that calling delete on a nullptr is fine. It won't do anything, so we don't need a conditional. We steal something that is temporary anyway, so that is fast. And we make sure what we have isn't deleted by setting where we stole it from to nullptr. Now we have exception safe code with no extra copies.

We just need an lValue copy constructor (remember, we can't use rValue because we're stealing).

    Foo(const Foo& rhs) : m_bar((rhs.m_bar) ? new Bar(*rhs.m_bar) : nullptr) {;} 

And here's everything together.

class Bar
{
    int i;
public:
//     Bar() = default;
};

class Foo
{
    Bar* m_Bar;
public:
    Foo() : m_bar(new Bar()) {;}

    Foo(const Foo& rhs) : m_bar((rhs.m_bar) ? new Bar(*rhs.m_bar) : nullptr) {;}

    Foo(Foo&& rhs) : m_bar(rhs.m_bar) { rhs.m_bar = nullptr; }

    Foo& operator=(Foo&& rhs)
    {
        delete m_bar;
        m_bar = rhs.m_bar;
        rhs.m_bar = nullptr;
        return *this;
    }

    Foo& operator=(Foo rhs)
    {
        *this = std::move(rhs);
        return *this;
    }
    ~Foo() { delete m_bar; }
};

No comments:

Post a Comment