What is std::move?
std::move is a functionality in C++11 which allows the passing of resources without copying. Many common definitions of std::move involve "lvalues", "rvalues", and "xvalues" -- I am going to actively avoid using these terms. Although they appropriately describe how std::move works, they add more complication than clarification to those who are unfamiliar. For simplicity, I will try to describe a problem which can be solved by properly using std::move.
The Problem
Suppose that I want to write a program to describe how two people (Frank and Bob) can play catch using a ball. When they play catch, they should never create more than one ball (after all, this would be wasteful). This program may seem contrived, but it demonstrates an important principle: how can someone properly transfer a resource using C++11 code without unnecessary duplication?
#include <iostream>
#include <string>
#include <utility>
using namespace std;
int num_balls = 0;
class Ball {
public:
// Constructor ==> Creates ball (only once!)
Ball() : _inflated(true), _num_passes(0) { if (num_balls++ > 0) exit(-1); }
// Copy constructor ==> Error
Ball(const Ball &o) { exit(-1); }
// Move constructor ==> Passes the ball
Ball(Ball&& o) : _inflated(o._inflated), _num_passes(o._num_passes + 1) {
o._inflated = false; // Not necessarily "de-allocating" -- just setting to null
}
bool _inflated;
int _num_passes;
};
In our definition of the "Ball" class, we defined some interesting methods. First, we defined the constructor. This creates an inflated ball which has not been passed yet, and ensures that anyone calling this constructor again will trigger the program to exit with an error code.
Next, we defined something called the copy constructor. Although typically used to copy information from one object to another, we have written it to cause the program to exit: we do not want to copy the ball, we want to pass it! This is an abnormal use of the copy constructor, but it is used to demonstrate a point here.
Lastly, we defined the move constructor. This method is used when passing ownership of an object: when it is finished the "o" ball should be valid, but the "this" ball resource should not be used again. Clearly, this constructor will be useful when passing a ball between two people. The passer will have an invalid ball by the end of the operation, the catcher will have an inflated ball, and no new balls will be created.
Let's try making a person:
class Person {
public:
Ball _ball;
string _name;
Person(string name) : _name(name){}
Person(string name, Person &p) : _name(name), _ball(p._ball) {}
};
int main() {
Person frank("Frank"); // Makes Frank, with a new ball.
Person bob("Bob", frank); // Tries to make Bob, taking Frank's ball.
cout << "Ball passed successfully" << endl;
}
The code is pretty self-explanatory. First, we make Frank (which calls the normal constructor for his ball, creating it). Next, we try to make Bob, using Frank's ball.
If we try running this code, it returns an error! What went wrong?
The Bug
The fault lies on this line:
Person(string name, Person &p) : _name(name), _ball(p._ball) {}
When creating Bob, we tried to pass Frank's ball: "_ball(p._ball)". Unfortunately, passing p._ball like this caused the copy constructor to be called, causing our program to exit. If we wanted this handover to create a brand new ball (which was an exact copy of the first), we could fill out the copy constructor function (or use the default copy constructor). However, we want to only allocate a single ball! How can we avoid this senseless copying?
The Solution
This is the problem std::move (and the move constructor) is designed to solve! By changing the line to:
Person(string name, Person &p) : _name(name), _ball(std::move(p._ball)) {}
We cause the move constructor to be called for this ball, rather than the copy constructor. Now when we run the program, it runs to completion and prints "Ball passed successfully"! On a finer-grained level, a few things are happening. First, Frank and his ball are constructed. Next the ball's move constructor is called when creating Bob. After Bob has been constructed, he has an "inflated" ball which has a "num_passes" value of one. In contrast, Frank's ball is not "inflated", and has a "num_passes" value of zero -- unfortunately, the example starts to break down a little here, but the basic idea is that Frank has a "nullptr"-style reference to an object, and he no longer has ownership of the originally inflated ball. Rather, it has moved away to Bob.
The Takeaway
std::move and the move constructor are useful tools for transferring objects without unnecessarily copying them. This optimization is both useful and surprisingly common: creating a swap function for two objects? Consider using std::move. Pushing an object onto a data structure, when you don't need to use the object locally anymore? Consider using std::move.
Hopefully you feel empowered and enlightened! If you have questions or criticisms, let me know in the comments.
Comments
Post a Comment