Move Semantics in C++


Move semantics might sound like a scary term, but it's relatively easy to understand. This post will explain some of the basic concepts of move semantics, and how important they can be!

References

Before we begin with move semantics, let's do a quick refresher on references. Consider the code below:

int main() {
    std::vector<int> vect;
    function1(vect);
    function2(vect);
}

void function1(std::vector<int> v) {
    /*
     * This is passing an argument by value
     * Modification of "v" here won't modify the "vect" passed in from main()
     */
}

void function2(std::vector<int> &v) {
    /*
     * This is passing an argument by reference
     * Modification of "v" here modifies the "vect" passed in from main()
     */
}
  • When we pass by value, what happens internally is that new std::vector gets created by calling the copy constructor.
    • This is not ideal in most cases, and is expensive performance-wise & space-wise.
  • When we pass by reference, what happens internally is that we tell the function "Hey function, here is the address of my already available vector. Simply use that!". This is faster and doesn't use extra space.

lvalue & rvalue

Although it technically go into a lot of depth, we'll try to get the fundamentals going first.
lvalue: Something that has a location, like in memory.
Example:

// `a` here is an lvalue. `a` is stored somewhere in our memory.
int a = 10;

rvalue: rvalues are the inverse of lvalues as in they don't have any location set. They're just there, temporarily.
Example:

// "10" here is an rvalue. 
int a = 10;

Now, you should be able to differentiate on what's what. Some more things you need to know:

  • lvalue can be assigned either an lvalue or an rvalue too.
int a = 10; // rvalue assignment to lvalue
int b = a;  // lvalue assignment to lvalue
  • You cannot assign something to an rvalue.
int a;
10 = a; // ERROR: What is 10? compiler doesn't know anything about it, and it's not somewhere we can write our memory to.

lvalue references & rvalue references

lvalue references: We've seen these before! In the "References" topic above, the code:

void function2(std::vector<int> &v) { ... } // v is an lvalue reference

We have few more rules related to lvalues & rvalues.

  • lvalue references cannot take in rvalues

Take the following code

// Function taking in lvalue reference
void printVal(int &num) {
    std::cout << "Value is: " << num << std::endl;
}

int main() {
    int a = 100;
    printVal(a); // Ok, we're passing lvalue to an lvalue reference
    printVal(10); // NOT OK!
}

Why is it not okay? Remember earlier, we said that

Hey function, here is the address of my already available "variable". Simply use that!
When we pass in 10, is that really a place in memory the compiler can refer to? It isn't! So, that's wrong.

rvalue references: rvalue references can be best explained with an example.

// Function taking in rvalue reference
void printVal(int &&num) {
    std::cout << "Value is: " << num << std::endl;
}
  • rvalue references cannot take in lvalues. So, let's take our same example from before:
int main() {
    int a = 100;
    printVal(a); // NOT OK!
    printVal(10); // Ok, we pass in rvalue reference.
}

Why is lvalue not accepted into an rvalue reference? Remember, rvalue means "temporary". lvalues have some sort of location, and they're not temporary, hence the "NOT OK!".

Move Semantics (Since C++11)

Finally, we've arrived to the topic of move semantics! At it's very basics, move semantics is moving ownership of objects around. Don't understand what that means? No worries, let's start with a simple example.

int main() {
    std::vector<int> vect = {10, 20, 30, 40, 50};
    ...
    ...
    // If we don't want to use the "vect" anymore in the current scope, 
    function1(std::move(vect)); // Passing in vect

    // Cannot use "vect" anymore, points to nullptr!
}

void function1(std::vector<int> &&v) {
    ... // Do things with v
}

That wasn't really a perfect example, but it should provide you with a good idea of what's trying to happen underneath when we use move semantics. One homework for you is to try to see some real life examples with this!

One question you might have is how come you can't use vect anymore in the orignal scope? That's because the standard library implementation of std::vector provides an inbuilt move constructor for us.

Move Constructor

Above we saw an example of move semantics in action, but didn't see how the move was actually happening. Here's a simple example of what a move constructor might look like:

// Suppose we're implementing a custom vector of integers class and it's move constructor.
class Vect {
private:
    int *_data;
    int _size;
public:
    Vect() {

    }
    ...
    ...

    Vect(Vect &&other) {
        _size = other._size; // The new object's size copied from existing size
        _data = other._data; // The new object's pointer points to the existing data (of the other string)

        // The below steps are important! Moving means we completely get rid of the orignal resources & transfer them to the
        // new requested "other" structure.
        other._size = 0;
        other._data = nullptr;
    }
    ...
    ...
    ~Vect() {

    }
};

Now, when function1(std::move(vect)); is called, the vect is moved into the new stack call of function1, which effectively avoids a copy. This way, move semantics offer great speed enhancements.


Move semantics, what's next?

Good question! Now that you're familiar with the basics, try finding some information on it's usecases in real life scenarios. Try writing a program that uses move semantics! Good luck on your journey!