Copy and move
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:
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}
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:
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}
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.
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}
check(Ticket t) takes its parameter by value, so every call makes a copy. Write the parameter as const Ticket& t and there are none.
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=:
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}
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:
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}
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":
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}
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:
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}
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 write | It 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.
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.
Ana: 100
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}
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.
Weekend plan: go hiking Weekend plan (copy): go hiking
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}
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.)
Moved New: 3, old: 0
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}
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 assignmentT& operator=(const T& other), which returns*this. = deleteafter the copy constructor and copy assignment forbids copies (use of deleted function); pass such objects by reference.std::move(x)saysxmay be moved from; a move constructorT(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
stringandvector, write none.