Intro to Move Semantics

Yadi Yasheng
4 min readJan 9, 2020

--

Wtf are we moving here?

Photo by Erda Estremera on Unsplash

If you search around about std::move, you usually get some definitions like this, “move basically just casts an lvalue to an rvalue”.

So what are lvalues & rvalues

It’s might be natural to think of lvalue and rvalues as left and right-hand side values. But that way of thinking will only make it even more confusing.

std::string str;
str + str = str;
// rvalue // lvalue

Instead of looking at them as left and right-hand side values, try to differentiate them by their significance. In this example, on the right, we have str which is a local variable. On the left-hand side, even though str itself is a local variable, but the result of (str + str) is a temporary data. Temporary data is not persistent and less “significant” than a local variable. Approaching it that way can help us more easily identify that (str + str) is rvalue.

Why do we need them

Let’s look at some code

struct Tile
{
public:
int x, y; Tile() = default;
~Tile() = default;
Tile(const Tile& t) :x(t.x), y(t.y)
{
std::cout << "copied\n";
}
}
class Grid
{
public:
Tile GetTileAtCoord(int x, int y)
{
Tile result;
for (const auto& tile : tiles)
{
if (tile.x == x && tile.y == y)
{
result = tile;
return result;
}
}
// not found
return result;
}
private:std::vector<Tile> tiles;};int main()
{
Grid grid;
Tile result = grid.GetTileAtCoord(1, 1); return 0;
}
output:
> copied!

This line below will create a dup of the returned tile, how do we avoid the extra copy?

Tile result = grid.GetTileAtCoord(1, 1));

we could update GetTileAtCoord() to return by reference and then we can capture the return value by reference

const Tile& GetTileAtCoord(int x, int y){}const Tile& result = grid.GetTileAtCoord(1, 1));

This works, but what do we return in the failure case?

const Tile& GetTileAtCoord(int x, int y)
{
for (const auto& tile : tiles)
{
if (tile.x == x && tile.y == y)
{
return tile;
}
}
// ???
return ????
}

Because we need to return by reference, we would have to allocate an invalid tile on the heap just to return it. Sure we can save unnecessary copies on the stack, but it’s not worth allocating stuff on the heap. Maybe use an out param?

…Imagine something just works in C++ for once.

Move Semantics, actually just works:

Tile t1;
Tile2 t2 = std::move(t1);

I don’t like the name “move”. It just makes things even more confusing. I like to think of it as “steal”. Basically what happens is, t2 steals all the resources of t1, and leaves t1 in a valid but undefined state(valid because it compiles and won’t crash, undefined because t1 doesn’t have/own his resources anymore). It’s more like transferring ownership without duplicating resources.

Now you might ask, how does it actually move or steal? What’s the implementation detail?

The answer is, it doesn’t actually do any of that.

std::move itself actually doesn’t do anything other than casting an lvalue to an rvalue. If you look up the implementation, it’s literally a static cast. It doesn’t even do anything at runtime.

But why the fuck do we need to cast an lvalue to an rvalue? What is it even for?

We need to cast the thing we want to avoid copying to an rvalue so that it will use this new thing they added called MoveConstructor/Assignment instead of CopyConstructor/Assignment. Let’s add those to the Tile class and take a look:

truct Tile
{
public:
....
Tile(Tile&& t) noexcept :x(std::move(t.x)), y(std::move(t.y))
{
std::cout << "moved!\n";
};
Tile& operator=(Tile&& t) noexcept
{
x = std::move(t.x);
y = std::move(t.y);
std::cout << "move!\n";
return *this;
}
}
// GetTileAtCoord just returns everything by value, nothing special...Tile result = grid.GetTileAtCoord(1, 1));...
output:
> moved!

No more copying baby.

Ok, I think I’ll stop here for now. This is just an intro. Unfortunately, nothing just works as we hoped for. This little std::move shit is just another rabbit hole and it goes real deep. If you’re interested, I’d like to share some questions I had when I was learning about these. I like to learn stuff with questions in mind beforehand, I found it very helpful.

  1. in the example above, Tile is just a simple struct with primitive type members. What if it has raw pointer members? (yes, it’s all on us to clean everything up. remember it might be called move, but it doesn’t do jack shit)
  2. why is noexcept needed? (if not careful, it will end up copying even after we’ve put in all the work to make the shit movable)
  3. do I have to explicitly define/override MoveConstructor/MoveAssignment? (not always. the compiler will generate them for ya in most cases, but there are 50 rules behind it. So I recommend reading it through on your own. Also, related to question 1, if there are raw pointers, you’re out of luck. )

--

--

Yadi Yasheng
Yadi Yasheng

Written by Yadi Yasheng

software engineer & game developer

No responses yet