Kodewick
Lesson 18 of 30Contents

Copy and move

Lesson 18 of 30 · Part 4 · Classes · about 15 minutes

Assign an int to another variable and you get a copy, and objects work the same way: after Playlist b = a;, b is a new playlist, and its songs are copies too. Changing b doesn't touch a:

main.cpp
1#include <iostream>2#include <string>3#include <vector>4 5class Playlist {6public:7    void add(const std::string& song) {8        songs_.push_back(song);9    }10 11    int count() const {12        return songs_.size();13    }14 15private:16    std::vector<std::string> songs_;17};18 19int main() {20    Playlist a;21    a.add("Track one");22    a.add("Track two");23    Playlist b = a;24    b.add("Track three");25    std::cout << a.count() << " " << b.count() << '\n';26    return 0;27}
Output
2 3

Press Step and see a and b drawn as two objects, each with its own row of songs. This lesson covers when copies happen, how to forbid them, and moving, which is cheaper than copying.

The copy constructor

Making a new object from an existing one calls the copy constructor. Above, you didn't write one, so the compiler made one: it copies member by member. Write your own that shows a line, and you can see when copies happen:

main.cpp
1#include <iostream>2#include <string>3 4class Playlist {5public:6    Playlist(const std::string& name) : name_(name) {}7 8    Playlist(const Playlist& other) : name_(other.name_) {9        std::cout << "Copied " << name_ << '\n';10    }11 12private:13    std::string name_;14};15 16void by_value(Playlist p) {17}18 19void by_ref(const Playlist& p) {20}21 22int main() {23    Playlist a{"Running"};24    Playlist b = a;25    Playlist c{a};26    by_value(a);27    by_ref(a);28    return 0;29}
Output
Copied Running
Copied Running
Copied Running

The copy constructor's parameter is const Playlist&: it copies from other without changing it. Playlist b = a; and Playlist c{a}; are both copies, and so is passing by value (lesson 8's "the function gets a copy" is its work). by_ref takes a reference, so nothing is copied.

Guess first

check is called 3 times. How many "copy"s are shown?

1#include <iostream>2 3class Ticket {4public:5    Ticket(int number) : number_(number) {}6 7    Ticket(const Ticket& other) : number_(other.number_) {8        std::cout << "copy ";9    }10 11private:12    int number_;13};14 15void check(Ticket t) {16}17 18int main() {19    Ticket t{7};20    for (int i = 0; i < 3; i++) {21        check(t);22    }23    std::cout << '\n';24    return 0;25}

Copy assignment

b = a; looks a lot like Playlist b = a;, but it's different: here b already exists, and a's contents are copied over the old ones. That calls the copy assignment operator, operator=:

main.cpp
1#include <iostream>2#include <string>3 4class Playlist {5public:6    Playlist(const std::string& name) : name_(name) {}7 8    Playlist(const Playlist& other) : name_(other.name_) {9        std::cout << "Copy constructor: " << name_ << '\n';10    }11 12    Playlist& operator=(const Playlist& other) {13        name_ = other.name_;14        std::cout << "Copy assignment: " << name_ << '\n';15        return *this;16    }17 18private:19    std::string name_;20};21 22int main() {23    Playlist a{"Running"};24    Playlist b{"Bedtime"};25    Playlist c = a;26    b = a;27    return 0;28}
Output
Copy constructor: Running
Copy assignment: Running

Look at whether the left side is a new variable being declared: Playlist c = a; declares a new c, so it's the copy constructor; in b = a;, b already exists, so it's copy assignment. Like += last lesson, operator= returns a reference to *this.

= delete: no copies allowed

Some things make no sense as copies. Copy a bank account and both copies hold the same $100; whose money is it? Write = delete after the copy constructor and copy assignment to delete them, and any copy is a compile error:

main.cpp
1#include <iostream>2#include <string>3 4class Account {5public:6    Account(const std::string& owner, int balance) : owner_(owner), balance_(balance) {}7    Account(const Account&) = delete;8    Account& operator=(const Account&) = delete;9 10    int balance() const {11        return balance_;12    }13 14private:15    std::string owner_;16    int balance_;17};18 19int main() {20    Account mine{"Ana", 100};21    Account copy = mine;22    std::cout << copy.balance() << '\n';23    return 0;24}
Output
main.cpp: In function 'int main()':
main.cpp:21:20: error: use of deleted function 'Account::Account(const Account&)'
   21 |     Account copy = mine;
      |                    ^~~~

use of deleted function 'Account::Account(const Account&)': a deleted function was used. Pass objects that can't be copied to functions by reference (const Account& or Account&).

Moving: hand it over, don't copy

Copying a playlist of thousands of songs means copying every song. But if you don't need the original any more, just move its contents over, which is much faster. std::move (in <utility>) says "this one may be moved from":

main.cpp
1#include <iostream>2#include <string>3#include <utility>4#include <vector>5 6int main() {7    std::string draft = "Today's diary";8    std::vector<std::string> diary;9    diary.push_back(draft);10    diary.push_back(std::move(draft));11    std::cout << diary.size() << '\n';12    std::cout << "[" << draft << "]\n";13    return 0;14}
Output
2
[]

The first push_back(draft) copies, and draft is still there; on the second, push_back(std::move(draft)), the text is moved into the vector and draft is left empty. Press Step to see at which step draft becomes empty.

Note: A moved-from object can still be given a new value and can die normally, but the standard only says its contents are "valid but unspecified" (a vector's move constructor does leave it empty; a string is empty on g++). So after moving from something, don't read it; give it a new value first if you want to use it again.

The move constructor

Your own classes can have a move constructor too. Its parameter is Playlist&&; the two &s mean "something that may be moved from". It moves each member over with std::move:

main.cpp
1#include <iostream>2#include <string>3#include <utility>4#include <vector>5 6class Playlist {7public:8    Playlist(const std::string& name) : name_(name) {}9 10    Playlist(Playlist&& other) : name_(std::move(other.name_)), songs_(std::move(other.songs_)) {11        std::cout << "Moved " << name_ << '\n';12    }13 14    void add(const std::string& song) {15        songs_.push_back(song);16    }17 18    int count() const {19        return songs_.size();20    }21 22private:23    std::string name_;24    std::vector<std::string> songs_;25};26 27int main() {28    Playlist old_list{"Running"};29    old_list.add("Track one");30    old_list.add("Track two");31    Playlist new_list = std::move(old_list);32    std::cout << new_list.count() << " " << old_list.count() << '\n';33    return 0;34}
Output
Moved Running
2 0

All the songs are in new_list, and old_list has 0. Press Step and watch the row of songs_ go from one object to the other in one piece, without a single song being copied. The program reads old_list.count() only to show it's empty: a moved-from vector is guaranteed to be empty. Normally, as the Note above says, don't read a moved-from object.

std::move doesn't move anything by itself; it only labels the object "may be moved from". The move constructor does the actual work. If you wrote a copy constructor but no move constructor, the label changes nothing and it's copied. (If you wrote neither, the compiler makes a move constructor for you.)

The rule of five

The destructor, copy constructor, copy assignment, move constructor and move assignment go together: if you write one of them, you probably need to think about the other four. This is the "rule of five" (it used to be just the first three, the rule of three).

The good news is that most classes need none of them. When the members are std::string, std::vector, int and the like, the five the compiler makes already copy, move and clean up correctly; this is sometimes called the "rule of zero". This lesson writes them only so you can see when they're called.

You writeIt calls
Playlist b = a; / Playlist b{a};copy constructor
b = a; (b already exists)copy assignment
Playlist b = std::move(a);move constructor
b = std::move(a); (b already exists)move assignment
passing by value, f(a)copy constructor

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Fix it: accounts can't be copied

Account can't be copied, so the program doesn't compile. Don't change Account; change only report so the program shows Ana: 100.

It should print
Ana: 100
exercise1.cpp
1#include <iostream>2#include <string>3 4class Account {5public:6    Account(const std::string& owner, int balance) : owner_(owner), balance_(balance) {}7    Account(const Account&) = delete;8    Account& operator=(const Account&) = delete;9 10    void show() const {11        std::cout << owner_ << ": " << balance_ << '\n';12    }13 14private:15    std::string owner_;16    int balance_;17};18 19void report(Account acc) {20    acc.show();21}22 23int main() {24    Account mine{"Ana", 100};25    report(mine);26    return 0;27}
Output
Press Run or Ctrl+Enter
Exercise 2

A copy says it's a copy

Write a copy constructor for Document: add " (copy)" to the end of the title and copy the text as it is. The program should show the two lines below.

It should print
Weekend plan: go hiking
Weekend plan (copy): go hiking
exercise2.cpp
1#include <iostream>2#include <string>3 4class Document {5public:6    Document(const std::string& title, const std::string& text) : title_(title), text_(text) {}7 8    void show() const {9        std::cout << title_ << ": " << text_ << '\n';10    }11 12private:13    std::string title_;14    std::string text_;15};16 17int main() {18    Document plan{"Weekend plan", "go hiking"};19    Document copy = plan;20    plan.show();21    copy.show();22    return 0;23}
Output
Press Run or Ctrl+Enter
Exercise 3

Move it, don't copy it

Inbox has only a copy constructor, so even std::move copies. Add a move constructor that shows "Moved" and moves messages_ over. The program should show the two lines below. (Reading old_box at the end is only to show it's empty: a moved-from vector is guaranteed to be empty.)

It should print
Moved
New: 3, old: 0
exercise3.cpp
1#include <iostream>2#include <string>3#include <utility>4#include <vector>5 6class Inbox {7public:8    Inbox(const std::vector<std::string>& messages) : messages_(messages) {}9 10    Inbox(const Inbox& other) : messages_(other.messages_) {11        std::cout << "Copied\n";12    }13 14    int count() const {15        return messages_.size();16    }17 18private:19    std::vector<std::string> messages_;20};21 22int main() {23    Inbox old_box{{"Hello", "Meeting", "Bill"}};24    Inbox new_box = std::move(old_box);25    std::cout << "New: " << new_box.count() << ", old: " << old_box.count() << '\n';26    return 0;27}
Output
Press Run or Ctrl+Enter

What you learned

  • Assigning an object to a new variable, or passing it by value, makes an independent copy with the copy constructor T(const T& other).
  • For an object that already exists, b = a; calls copy assignment T& operator=(const T& other), which returns *this.
  • = delete after the copy constructor and copy assignment forbids copies (use of deleted function); pass such objects by reference.
  • std::move(x) says x may be moved from; a move constructor T(T&& other) moves the members over instead of copying.
  • A moved-from object can only be given a new value or left to die; don't read its value.
  • The five special member functions (destructor, copy constructor, copy assignment, move constructor, move assignment): write one, think about the rest. When the members are things like string and vector, write none.