C++ Move Semantics
Move semantics is a way to transfer the contents of one object to another without copying them. The source object is left empty and safe to destroy.
Passing by value copies
The showSize function takes its Buffer by value. The copy counter proves that the call made one full copy of the buffer.
#include <iostream>
class Buffer {
public:
static int copies;
int size;
int* data;
Buffer(int n) {
size = n;
data = new int[n];
data[0] = 7;
}
Buffer(const Buffer& other) {
copies++;
size = other.size;
data = new int[size];
data[0] = other.data[0];
}
~Buffer() {
delete[] data;
}
};
int Buffer::copies = 0;
void showSize(Buffer b) {
std::cout << "Inside size: " << b.size << std::endl;
std::cout << "Inside first: " << b.data[0] << std::endl;
}
int main() {
Buffer buf(100);
showSize(buf);
std::cout << "Copies: " << Buffer::copies << std::endl;
std::cout << "Original size: " << buf.size << std::endl;
return 0;
}A move constructor takes the guts
The move constructor takes the array from the source and sets the size of the source to 0 by hand. The moved-from buffer prints its size as 0.
#include <iostream>
#include <utility>
class Buffer {
public:
static int copies;
static int moves;
int size;
int* data;
Buffer(int n) {
size = n;
data = new int[n];
data[0] = 7;
}
Buffer(const Buffer& other) {
copies++;
size = other.size;
data = new int[size];
data[0] = other.data[0];
}
Buffer(Buffer&& other) {
moves++;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
}
~Buffer() {
delete[] data;
}
};
int Buffer::copies = 0;
int Buffer::moves = 0;
int main() {
Buffer b1(100);
Buffer b2 = std::move(b1);
std::cout << "B1 size: " << b1.size << std::endl;
std::cout << "B2 size: " << b2.size << std::endl;
std::cout << "Moves: " << Buffer::moves << std::endl;
std::cout << "Copies: " << Buffer::copies << std::endl;
return 0;
}std::move chooses the move
The first construction has no std::move and copies. The second construction adds std::move and moves, and the two counters prove which path ran each time.
#include <iostream>
#include <utility>
class Buffer {
public:
static int copies;
static int moves;
int size;
int* data;
Buffer(int n) {
size = n;
data = new int[n];
data[0] = 7;
}
Buffer(const Buffer& other) {
copies++;
size = other.size;
data = new int[size];
data[0] = other.data[0];
}
Buffer(Buffer&& other) {
moves++;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
}
~Buffer() {
delete[] data;
}
};
int Buffer::copies = 0;
int Buffer::moves = 0;
int main() {
Buffer a(50);
Buffer b = a;
std::cout << "After copy: copies " << Buffer::copies << " moves " << Buffer::moves << std::endl;
Buffer c = std::move(a);
std::cout << "After move: copies " << Buffer::copies << " moves " << Buffer::moves << std::endl;
return 0;
}Move assignment
Both buffers exist before the assignment line. The move assignment frees the old array of b, takes the array of a, and leaves a empty.
#include <iostream>
#include <utility>
class Buffer {
public:
static int copies;
static int moves;
int size;
int* data;
Buffer(int n) {
size = n;
data = new int[n];
data[0] = 7;
}
Buffer(const Buffer& other) {
copies++;
size = other.size;
data = new int[size];
data[0] = other.data[0];
}
Buffer(Buffer&& other) {
moves++;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
}
Buffer& operator=(Buffer&& other) {
if (this != &other) {
moves++;
delete[] data;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
}
return *this;
}
~Buffer() {
delete[] data;
}
};
int Buffer::copies = 0;
int Buffer::moves = 0;
int main() {
Buffer a(80);
Buffer b(20);
b = std::move(a);
std::cout << "A size: " << a.size << std::endl;
std::cout << "B size: " << b.size << std::endl;
std::cout << "Moves: " << Buffer::moves << std::endl;
return 0;
}Example
#include <iostream>
#include <utility>
class Buffer {
public:
static int copies;
static int moves;
int size;
int* data;
Buffer(int n) {
size = n;
data = new int[n];
data[0] = 7;
}
Buffer(const Buffer& other) {
copies++;
size = other.size;
data = new int[size];
data[0] = other.data[0];
}
Buffer(Buffer&& other) {
moves++;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
}
Buffer& operator=(Buffer&& other) {
if (this != &other) {
moves++;
delete[] data;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
}
return *this;
}
~Buffer() {
delete[] data;
}
};
int Buffer::copies = 0;
int Buffer::moves = 0;
int main() {
Buffer first(60);
Buffer second(30);
Buffer shelf = std::move(first);
std::cout << "Shelf: " << shelf.size << std::endl;
shelf = std::move(second);
std::cout << "Shelf: " << shelf.size << std::endl;
std::cout << "First: " << first.size << std::endl;
std::cout << "Second: " << second.size << std::endl;
std::cout << "Moves: " << Buffer::moves << std::endl;
return 0;
}Key points
- Passing an object by value can copy it, and a copy of a large object costs time and memory.
- A move constructor builds a new object by taking the members of a source object.
- Its parameter is written with && after the class name.
- A move constructor leaves the source in a state the class itself chooses.
- Writing std::move at a call site asks for a move instead of a copy.
- Move assignment moves into an object that already exists.
In this example, the shelf first takes the array of first by a move construction. The shelf then takes the array of second by a move assignment. Both sources print size 0, and the move counter reaches 2.
Note
A moved-from object can still be destroyed and can still be assigned a new value.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What does std::move at a call site do?