Copy and Move Semantics

The Rule of Zero

Most classes never need to be in a position to write a destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator. If every member of a class already knows how to manage its own lifetime, then a compiler generated version of any of these five functions will do precisely what is needed. That is the Rule of Zero.

Here's an instance of a class observing it:

#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Person
{
private:
    std::string m_name;
    std::vector<int> m_scores;

public:
    Person(std::string name, std::vector<int> scores)
        : m_name{ std::move(name) }
        , m_scores{ std::move(scores) }
    {
    }

    const std::string& name() const
    {
        return m_name;
    }

    int scoreCount() const
    {
        return static_cast<int>(m_scores.size());
    }
};

int main()
{
    Person p{ "Alex", { 90, 85, 92 } };
    std::cout << p.name() << ' ' << p.scoreCount() << '\n';
    return 0;
}

Person is comprised of two members, a std::string and a std::vector<int>. Both of those existing library types properly implement all five special member functions. Consequently, a compiler generated destructor for Person simply has the effect of calling the destructor for m_name followed by the destructor for m_scores; these will, in turn, free any heap allocated memory they own. There's no need for an additional destructor body in Person.

The same sort of story obtains for copying and moving. A compiler generated copy constructor will have the effect of invoking the copy constructor for std::string and the copy constructor for std::vector, each doing a deep copy. A move constructor similarly invites the move constructor of each member, transferring ownership without further fuss. As Person itself doesn't touch a raw resource, the compiler generated default can't go wrong.

Compare that with a class that breaks the Rule of Zero by managing a raw resource more directly. The moment that a user declared destructor appears, the compiler will cease generating the move operations, and its copy operations become shallow. Such a class now falls under the Rule of Five and must hand craft all five operations. The Rule of Zero is principally designed to save classes from this fate.

A second example illustrates how extreme the principle can be carried:

#include <iostream>
#include <memory>
#include <string>
#include <utility>

class Document
{
private:
    std::string m_title;
    std::unique_ptr<std::string> m_body;

public:
    Document(std::string title, std::string body)
        : m_title{ std::move(title) }
        , m_body{ std::make_unique<std::string>(std::move(body)) }
    {
    }

    const std::string& title() const
    {
        return m_title;
    }

    const std::string& body() const
    {
        return *m_body;
    }
};

int main()
{
    Document d{ "Report", "Quarterly results look good." };
    std::cout << d.title() << '\n';
    std::cout << d.body() << '\n';
    return 0;
}

Even with a dynamically allocated string as its only owned resource, Document declares neither a destructor, nor any copying or moving operations. A std::unique_ptr manages that string, its destructor is appropriately well formed; its copy operations are implicitly deleted. Those same properties are inherited by the default special members of Document, making it a movable (but not copyable) type with a proper destructor. Again, no hand crafted special member code appears.

The resulting idiom is that most day to day classes remain remarkably compact: they are little more than their constructors (which initialize member objects) and their normal behavior functions, leaving their five special members hidden to a person as an interesting, but rarely present, technicality.

If such a class subsequently requires management of some novel resource type not supported elsewhere, its control over that raw data can be encapsulated within a self contained helper type subject to the Rule of Five. Everything else adheres to the Rule of Zero.

Exercise: The Rule of ZeroStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: The Rule of Zero

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Copy-construct a Person from p, then move-construct another Person from that copy. Print the moved result and original p without assuming any particular contents for the moved-from copy.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Person
{
private:
    std::string m_name;
    std::vector<int> m_scores;

public:
    Person(std::string name, std::vector<int> scores)
        : m_name{ std::move(name) }
        , m_scores{ std::move(scores) }
    {
    }

    const std::string& name() const
    {
        return m_name;
    }

    int scoreCount() const
    {
        return static_cast<int>(m_scores.size());
    }
};

int main()
{
    Person p{ "Alex", { 90, 85, 92 } };
    std::cout << p.name() << ' ' << p.scoreCount() << '\n';
    return 0;
}

Hints

Hint 1

Person stores a std::string and std::vector, which already manage their own resources. That is why Person can omit custom copy, move, and destructor definitions.

Hint 2

Try Person q{p} to copy the name and scores safely. The std::move calls in the ordinary constructor initialize members from its local parameters; they are separate from copying p.

Worked solution

copy is copy-constructed from p, and moved is move-constructed from copy. std::string and std::vector handle resource ownership, so Person needs no custom special members. The source p stays unchanged; no particular content is assumed for moved-from copy.

#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Person
{
private:
    std::string m_name;
    std::vector<int> m_scores;

public:
    Person(std::string name, std::vector<int> scores)
        : m_name{ std::move(name) }
        , m_scores{ std::move(scores) }
    {
    }

    const std::string& name() const
    {
        return m_name;
    }

    int scoreCount() const
    {
        return static_cast<int>(m_scores.size());
    }
};

int main()
{
    Person p{ "Alex", { 90, 85, 92 } };
    std::cout << p.name() << ' ' << p.scoreCount() << '\n';
    Person copy{p};
    Person moved{std::move(copy)};
    std::cout << "Copied then moved: " << moved.name() << ' ' << moved.scoreCount() << '\n';
    std::cout << "Original: " << p.name() << ' ' << p.scoreCount() << '\n';
    return 0;
}

Example output

Alex 3
Copied then moved: Alex 3
Original: Alex 3

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.