๐ŸŽ“ A1Learner
๐Ÿ”
0 of 34 lessons completed

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?