c++c++11moveswapmove-semantics

What happens in the memory when C++'s move semantics is used?


I am trying to understand C++'s move semantics, move constructor, move assignment operator, std::move(). Let's consider the following example:

#include <iostream>

void swapWithMove(int& a, int& b) {
    int temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

int main() {
    int x = 5;
    int y = 10;

    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;

    swapWithMove(x, y);

    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;

    return 0;
}

This example exchanges two integer values via std::move(). I know that the example is more for educational purposes and is not the typical use case of move semantics. But still, I would like to understand properly what happens in the memory to x, y, temp, a and b when the swapWithMove function is being executed.

Thank you very much in advance!


Solution

  • Under the circumstances (swapping ints), move semantics won't make any difference at all, with any implementation of which I'm aware (and an exception would be somewhat surprising).

    Move semantics largely come into to play when we're dealing with something like a vector that (mostly) stores a pointer to the data it contains. For example, let's consider a somewhat simplified implementation of std::vector:

    template <class T>
    class vector {
        T *data;
        std::size_t size_allocated;
        std::size_t size_in_use;
    // ...
    
    

    The big thing to notice here is that the vector structure itself doesn't contain any of the real data. It just contains a pointer to the data, which is allocated wherever its Allocator object gets it (but typically using the standard allocator, which gets memory from the free store.

    Anyway, let's consider assignment for our vector:

    vector &operator=(vector const &other) {
        if (size_allocated < other.size_allocated) {
            // reallocate our storage so we have enough room
        }
        size_in_use = other.size_in_use;    
        for (std::size_t i=0; i<other.size_in_use; i++)
            data[i] = other.data[i];
        return *this;
    }
    

    That's simplified a lot, but you get the general idea--step through all the elements, and copy each one from the old vector to the new one.

    But, if the right-hand one is an rvalue, that means we don't need to preserve its contents. So we can just "steal" what it contains:

    vector &operator=(vector &&other) {
        delete [] data; // simplified--really uses allocator object
        data = other.data;
        size_in_use = other.size_in_use;
        size_allocated = other.size_allocted;
        other.data = nullptr;
        other.size_allocated = 0;
        other.size_in_use = 0;
        return *this;
    }
    

    So, instead of individually copying each element, we just grab the pointer from the source, and turn the source into an empty vector. Very fast, regardless of its size. There is a slightly tricky way we can simplify this a bit though:

    vector &operator=(vector &&other) {
        swap(data, other.data);
        swap(size_in_use, other.size_in_use);
        swap(size_allocated, other.size_allocated);
    
        return *this;
    }
    

    Just swap our contents with the other's contents. It'll then be destroyed, and what used to be our contents will be disposed of then.

    Anyway: moving an int is generally no different from just a normal assignment. Same with most other scalar types (char, short, long, double, float, pointers, etc.) The differences arise for structured types, especially those that mostly contain a pointer to the real data they store.