Copying and Moving Objects

When an object is copied, assigned or moved, C++ calls special member functions. The compiler writes them for you, and for most classes they're exactly right. This lesson shows what they are, when you must write your own, and the rules of zero, three and five that keep you out of trouble.

The special member functions

FunctionRuns when
Copy constructor T(const T&)A new object is made as a copy: T b = a;
Copy assignment T& operator=(const T&)An existing object is overwritten: b = a;
Move constructor T(T&&)A new object takes over a temporary or std::moved object
Move assignment T& operator=(T&&)An existing object takes over another's contents
Destructor ~T()The object's lifetime ends

The compiler-generated versions copy, move or destroy each member in turn. Because std::string, std::vector and smart pointers already do the right thing, a class built from them needs no special functions of its own — the rule of zero.

Watching them run

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

class Widget {
public:
    explicit Widget(std::string name) : name_(std::move(name)) {}
    Widget(const Widget& other) : name_(other.name_) { std::cout << "copy " << name_ << '\n'; }
    Widget(Widget&& other) noexcept : name_(std::move(other.name_)) { std::cout << "move " << name_ << '\n'; }
    Widget& operator=(const Widget& other) { name_ = other.name_; std::cout << "copy-assign " << name_ << '\n'; return *this; }
    Widget& operator=(Widget&& other) noexcept { name_ = std::move(other.name_); std::cout << "move-assign " << name_ << '\n'; return *this; }
private:
    std::string name_;
};

Widget make() { return Widget{"made"}; }

int main() {
    Widget a{"a"};
    Widget b = a;                 // copy
    Widget c = std::move(a);      // move
    b = c;                        // copy-assign
    b = make();                   // move-assign from a temporary (make() itself copies nothing)
    std::vector<Widget> v;
    v.reserve(2);
    v.push_back(std::move(b));    // move into the vector
}
Output
copy a
move a
copy-assign a
move-assign made
move made

Note that make() printed nothing: returning a temporary constructs it directly in place (guaranteed copy elision). Mark move operations noexcept — containers like std::vector only use them during reallocation if they can't throw.

When you must write them: owning a raw resource

A class that directly owns a raw resource gets the wrong defaults. Copying a raw pointer copies the address, not the data — two objects then "own" the same memory and both delete it. Here's a minimal buffer class done properly, following the rule of five: if you write any of destructor, copy or move operations, write (or delete) all of them:

C++
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <utility>

class Buffer {
public:
    explicit Buffer(std::size_t size) : size_(size), data_(new int[size]{}) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {     // deep copy
        std::copy(other.data_, other.data_ + size_, data_);
    }
    Buffer& operator=(Buffer other) noexcept {      // copy-and-swap: handles copy and move assignment
        swap(*this, other);
        return *this;
    }
    Buffer(Buffer&& other) noexcept                  // steal the resource
        : size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {}

    friend void swap(Buffer& a, Buffer& b) noexcept {
        std::swap(a.size_, b.size_);
        std::swap(a.data_, b.data_);
    }

    int& operator[](std::size_t i) { return data_[i]; }
    std::size_t size() const { return size_; }
private:
    std::size_t size_;
    int* data_;
};

int main() {
    Buffer a(3);
    a[0] = 42;
    Buffer b = a;              // independent copy
    b[0] = 7;
    Buffer c = std::move(a);   // a is now empty
    std::cout << b[0] << ' ' << c[0] << ' ' << a.size() << ' ' << c.size() << '\n';
    b = c;
    std::cout << b[0] << '\n';
}
Output
7 42 0 3
42

That's a lot of code for something std::vector<int> already does — which is exactly the point. Write the rule of five only for classes whose job is managing a resource; everything else should hold such classes as members and follow the rule of zero.

Disabling copies

Some objects shouldn't be copied: a file handle, a mutex, a unique network connection. Delete the copy operations. Such types can often still be moved:

C++
#include <string>

class Connection {
public:
    explicit Connection(std::string host) : host_(std::move(host)) {}
    Connection(const Connection&) = delete;
    Connection& operator=(const Connection&) = delete;
    Connection(Connection&&) = default;
    Connection& operator=(Connection&&) = default;
private:
    std::string host_;
};

int main() {
    Connection a{"db.example.com"};
    Connection b = std::move(a);   // fine: moving
    Connection c = b;              // error: copying is deleted
}
Compiler error
main.cpp:17:16: error: call to deleted constructor of 'Connection'

Summary

  • Rule of zero: build classes from members that manage themselves, and write none of the special functions. This is the default.
  • Rule of five: if a class manages a raw resource and needs a custom destructor, it needs custom (or deleted) copy and move operations too.
  • Use = default to ask for the compiler's version explicitly, and = delete to forbid an operation.
  • Mark moves noexcept. A moved-from object must still be destructible and assignable.

Try it yourself

This class follows the rule of zero. Copy and move some objects and confirm everything behaves without any special functions written.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
Road trip (2 songs)
Road trip (3 songs)