Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 7 – Classes, objects and RAII

The rule of zero, three and five in C++

See when the compiler's copy of a C++ class is wrong, write the five special member functions for a raw buffer, and prefer the rule of zero.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain when the compiler-generated copy is wrong
  • Implement the five special member functions for a resource-owning class
  • Apply the rule of zero by composing standard types
  • Predict which special member functions the compiler declares

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain when the compiler-generated copy is wrong
  • Implement the five special member functions for a resource-owning class
  • Apply the rule of zero by composing standard types
  • Predict which special member functions the compiler declares

Before you start

On this page

Copy a class that owns an array from new[] with the compiler’s own copy, and two objects end up deleting the same array: AddressSanitizer stops the program at the second delete[] with attempting double-free. Five special member function: One of the member functions the compiler can write for a class by itself: the default constructor, the destructor, the copy constructor, copy assignment, the move constructor and move assignment. The compiler's versions work member by member. decide what happens when an object is copied, moved and destroyed: the destructor, the copy constructor, the copy assignment operator, the move constructor and the move assignment operator. If you do not write them, the compiler does, member by member, which is exactly right for members that manage themselves, such as std::string, std::vector and std::unique_ptr, and wrong for a raw owning pointer.

Three rules say which of the five to write: the rule of zero: Build a class from members that manage themselves, such as std::vector, std::string and std::unique_ptr, and declare none of the five copy, move and destruction functions: the compiler's member-by-member versions are then correct., the rule of three: A class that needs its own destructor, usually because it owns a raw resource, also needs its own copy constructor and copy assignment operator, because the compiler's versions would share the resource. and the rule of five: The rule of three plus the two move operations of C++11: a class that declares any of the destructor, the copy operations or the move operations defines or deletes all five, since declaring one changes the others.. This lesson reproduces the double free with Apple clang 21 and GCC 16, fixes it by hand, measures what noexcept on a move changes for std::vector, and shows the empty destructor that turns every move into a copy.

The compiler’s copy copies the pointer

Buffer owns an array of int made with new[] and deletes it in its destructor, as the previous lesson taught. It does not say how to copy itself, so the compiler writes a copy constructor that copies size_ and data_, a shallow copy: A copy that duplicates a pointer but not what it points to, so two objects share one resource. The compiler's copy of a class that owns memory through a raw pointer is one, and both copies later delete that memory.:

shallow_copy.cpp C++ · shallow_copy.cpp
// Buffer owns an array made with new[], but leaves copying to the compiler. The
// compiler's copy copies the pointer, not the array.
#include <cstddef>
#include <cstdio>
#include <print>

class Buffer {
public:
    explicit Buffer(std::size_t size) : size_{size}, data_{new int[size]{}} {}
    ~Buffer() { delete[] 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};
    Buffer b = a;    // the implicit copy constructor: b.data_ = a.data_
    b[0] = 99;
    std::println("a[0] = {} after writing b[0]", a[0]);
    std::println("main ends: b is destroyed, then a");
    std::fflush(stdout);   // print now: the sanitizer stops the program before the usual flush
}   // ~Buffer runs twice on the same array: a double free
A double free, caught by AddressSanitizer Python · shallow_copy_session.py
from session import run

# Built with AddressSanitizer (-fsanitize=address), which stops the program at the
# second delete[]. abort_on_error=0 makes it exit with status 1 instead of aborting.
# Its report goes to a file; grep shows the main lines, and sed replaces the
# addresses, which change from run to run, with 0x...
run("""
clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address shallow_copy.cpp -o shallow_copy
ASAN_OPTIONS=abort_on_error=0 ./shallow_copy 2> asan.txt
grep -E 'ERROR|freed by|previously allocated|shallow_copy.cpp|SUMMARY' asan.txt | sed -E 's/0x[0-9a-f]+/0x.../g; s/==[0-9]+==//'
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address shallow_copy.cpp -o shallow_copy-gcc
ASAN_OPTIONS=abort_on_error=0 ./shallow_copy-gcc 2> asan-gcc.txt
grep -E 'ERROR|freed by|previously allocated|shallow_copy.cpp|SUMMARY' asan-gcc.txt | sed -E 's/0x[0-9a-f]+/0x.../g; s/==[0-9]+==//'
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address shallow_copy.cpp -o shallow_copy
$ ASAN_OPTIONS=abort_on_error=0 ./shallow_copy 2> asan.txt
a[0] = 99 after writing b[0]
main ends: b is destroyed, then a
[exit status 1]
$ grep -E 'ERROR|freed by|previously allocated|shallow_copy.cpp|SUMMARY' asan.txt | sed -E 's/0x[0-9a-f]+/0x.../g; s/==[0-9]+==//'
ERROR: AddressSanitizer: attempting double-free on 0x... in thread T0:
    #1 0x... in Buffer::~Buffer() shallow_copy.cpp:10
    #2 0x... in Buffer::~Buffer() shallow_copy.cpp:10
    #3 0x... in main shallow_copy.cpp:27
freed by thread T0 here:
    #1 0x... in Buffer::~Buffer() shallow_copy.cpp:10
    #2 0x... in Buffer::~Buffer() shallow_copy.cpp:10
    #3 0x... in main shallow_copy.cpp:27
previously allocated by thread T0 here:
    #1 0x... in Buffer::Buffer(unsigned long) shallow_copy.cpp:9
    #2 0x... in Buffer::Buffer(unsigned long) shallow_copy.cpp:9
    #3 0x... in main shallow_copy.cpp:21
SUMMARY: AddressSanitizer: double-free shallow_copy.cpp:10 in Buffer::~Buffer()
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address shallow_copy.cpp -o shallow_copy-gcc
$ ASAN_OPTIONS=abort_on_error=0 ./shallow_copy-gcc 2> asan-gcc.txt
a[0] = 99 after writing b[0]
main ends: b is destroyed, then a
[exit status 1]
$ grep -E 'ERROR|freed by|previously allocated|shallow_copy.cpp|SUMMARY' asan-gcc.txt | sed -E 's/0x[0-9a-f]+/0x.../g; s/==[0-9]+==//'
ERROR: AddressSanitizer: attempting double-free on 0x... in thread T0:
    #1 0x... in Buffer::~Buffer() shallow_copy.cpp:10
    #2 0x... in main shallow_copy.cpp:27
freed by thread T0 here:
    #1 0x... in Buffer::~Buffer() shallow_copy.cpp:10
    #2 0x... in main shallow_copy.cpp:27
previously allocated by thread T0 here:
    #1 0x... in Buffer::Buffer(unsigned long) shallow_copy.cpp:9
    #2 0x... in main shallow_copy.cpp:21
SUMMARY: AddressSanitizer: double-free shallow_copy.cpp:10 in Buffer::~Buffer()

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shallow_copy_session.py

Step through the program and watch the one array that both objects point to:

Interactive · step by step Two Buffers, one array, two delete[] Step through shallow_copy.cpp. The left column holds a and b, the right one the array that new int[3]{} made.
Two Buffers, one array, two delete[], step 6 of 6. Showing Buffer a, Buffer b (destroyed), int[3] (freed). In focus: int[3].data_Buffer asize_ 3, data_Buffer bdestroyedint[3]freed

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Buffer a{3}. The constructor runs new int[3]{}, which makes three zeros on the free store, and data_ holds their address.
  2. Buffer b = a. Buffer declares no copy constructor, so the compiler's copies each member: size_ is 3, and data_ is the same address. No second array is made.
  3. b[0] = 99. Writing through b changes the one array, so a sees it too: the program printed a[0] = 99 after writing b[0].
  4. b is destroyed. At the closing brace of main, b is destroyed first, because it was made last. ~Buffer runs delete[] data_, and the array is freed.
  5. a deletes it again. Then ~Buffer runs for a, whose data_ still holds the freed address, and calls delete[] on it a second time. AddressSanitizer stops the program: attempting double-free, at line 10.
  6. Three stacks. The report gives the bad free, the first free (the same line, in b's destructor) and the allocation on line 9. Without the sanitizer this second delete[] is undefined behaviour.

The first line of output shows the sharing: writing b[0] changed a[0], because both pointers hold the same address. The report shows the cost. When main ends, b is destroyed and deletes the array, then a is destroyed and deletes it again. AddressSanitizer, which -fsanitize=address builds into the program with both compilers, stops at the second delete[] and prints three stacks: where the bad free happened, where the array was freed the first time (the same line, in another object’s destructor) and where it was allocated. Apple clang’s report shows two ~Buffer() frames where GCC’s shows one, because Clang emits two destructor functions for the class, the complete-object and the base-object destructor that the Itanium C++ ABI defines, and the program passes through both. The ASAN_OPTIONS=abort_on_error=0 setting only changes how the program stops: on macOS the sanitizer aborts by default, and with this flag it exits with status 1 instead.

The bug is in every build. Without the sanitizer, the second delete[] is undefined behaviour: the program may crash at that line, crash much later in unrelated code, or appear to work. The program calls std::fflush(stdout) before main ends because the sanitizer stops it without the usual flush of buffered output, which would lose the two lines it printed. The diagram puts the compiler’s copy beside the one the next section writes:

After the implicit copy, a and b point to one array; after a deep copy, each Buffer points to an array of its own.Implicit copy: Buffer b = a;Deep copy: Buffer(const Buffer&)asize_ = 3data_bsize_ = 3data_one heap array0 0 0asize_ = 3data_bsize_ = 3data_a's array0 0 0b's own array0 0 0the same addressnew int[3], then copiedfix

A shallow copy shares the array; a deep copy duplicates it

Text description of the diagram

The diagram has two panels.

  1. The first panel, "Implicit copy: Buffer b = a;", shows two Buffer objects, a and b, each with size_ = 3 and a data_ pointer. Both pointers lead to the same heap array holding 0, 0, 0. Writing b[0] changes what a sees, and when both objects are destroyed, the same array is deleted twice.
  2. A dashed arrow labelled "fix" leads to the second panel, "Deep copy: Buffer(const Buffer&)". Here a's pointer leads to a's array and b's pointer leads to a second array of its own, made with new int[3] and filled with a copy of a's values. Each destructor deletes only its own array.

Checkpoint

Check: The compiler's copy of a raw pointer

  1. Question 1 of 2 Buffer b = a; uses the compiler's copy constructor, then b[0] = 99;. What does a[0] hold?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 99

    A copy that duplicates the pointer but not the array is a shallow copy: two objects, one array. The bug that follows is the second delete[].

  2. Question 2 of 2 Where did AddressSanitizer's report put the bad free in shallow_copy.cpp?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In Buffer::~Buffer() at line 10, the delete[], reached from main's closing brace at line 27

    The report names the bad free, the first free (the same line, in another object's destructor) and the allocation in the constructor at line 9.

The rule of three: copies with their own array

A class that owns a resource has to say what copying means. For Buffer, a copy should get its own array with the same values: a deep copy: A copy that duplicates the resource itself, as a copy constructor does when it allocates a new array and copies the values into it, so the two objects own separate resources.. The rule of three names the functions involved, because they come as a set: a class that needs a destructor to release something also needs a copy constructor and a copy assignment operator that duplicate it. Here is Buffer with all five special member functions; numbers 1 to 3 are the rule of three:

rule_of_five.cpp C++ · rule_of_five.cpp
// Buffer again, now with all five special member functions written by hand: a copy
// gets its own array, and a move hands the array over.
#include <algorithm>
#include <cstddef>
#include <print>
#include <utility>

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

    ~Buffer() { delete[] data_; }                                  // 1. destructor

    Buffer(const Buffer& other)                                    // 2. copy constructor
        : size_{other.size_}, data_{new int[other.size_]} {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    Buffer& operator=(const Buffer& other) {                       // 3. copy assignment
        Buffer copy{other};   // copy first: if new[] throws, *this is unchanged
        swap(copy);           // take the copy's array; copy leaves with the old one
        return *this;
    }

    Buffer(Buffer&& other) noexcept                                // 4. move constructor
        : size_{std::exchange(other.size_, 0)}, data_{std::exchange(other.data_, nullptr)} {}

    Buffer& operator=(Buffer&& other) noexcept {                   // 5. move assignment
        Buffer moved{std::move(other)};
        swap(moved);
        return *this;
    }

    void swap(Buffer& other) noexcept {
        std::swap(size_, other.size_);
        std::swap(data_, other.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};
    Buffer b = a;              // copy constructor: a second array
    b[0] = 99;
    std::println("copy:  a[0] = {}, b[0] = {}", a[0], b[0]);

    Buffer& same = a;
    a = same;                  // self-assignment through a reference
    std::println("self:  a.size() = {}, a[0] = {}", a.size(), a[0]);

    Buffer c = std::move(b);   // move constructor: c takes b's array
    std::println("move:  c[0] = {}, b.size() = {}", c[0], b.size());

    b = c;                     // a moved-from object can be assigned again
    std::println("reuse: b.size() = {}, b[0] = {}", b.size(), b[0]);
}
  • The copy constructor allocates a new array of the same size and copies the values into it.
  • The copy assignment operator uses the copy-and-swap: A way to write copy assignment: make a complete copy of the source, swap its members with this object's, and let the copy's destructor free the old resource. It needs no test for self-assignment and changes nothing if the copy throws. idiom. It first makes a complete copy of other, then swaps its own members with the copy’s, and lets the copy’s destructor delete the old array. If new int[] throws while the copy is made, nothing in *this has changed yet, which is the strong exception guarantee. And self-assignment needs no special test: a = a copies a, swaps, and frees the old array, leaving the same values. Rule C.62 of the C++ Core Guidelines asks for exactly that safety.

The main function assigns through a reference, Buffer& same = a; a = same;, because that is how self-assignment happens in real code: two names for one object meet, as in v[i] = v[j] when i and j happen to be equal. Step through that assignment:

Interactive · step by step a = same, through copy-and-swap Step through the copy assignment operator when both names refer to the same Buffer. The right column holds the arrays.
a = same, through copy-and-swap, step 4 of 4. Showing a (also same), Buffer copy{other} (destroyed), the first array (freed by ~Buffer), a second array (0 0 0, a copy). In focus: the first array.ownsa (also same)size_ 3Buffer copy{other}destroyedthe first arrayfreed by ~Buffera second array0 0 0, a copy

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Two names, one object. Buffer& same = a; gives a a second name, so a = same; assigns an object to itself, as v[i] = v[j] does when i equals j.
  2. Copy first. Buffer copy{other}; runs the copy constructor: a second array of 3 ints, with the values copied in. If new int[] threw here, a would not have changed at all.
  3. Swap. swap(copy) exchanges size_ and data_: a now owns the second array, and copy owns the first.
  4. The copy takes the old array. At the end of operator=, copy is destroyed, and its destructor deletes the first array. a keeps the same values in its new array: the program printed a.size() = 3, a[0] = 0.

Checkpoint

Check: Deep copies and copy-and-swap

  1. Question 1 of 2 Buffer& same = a; a = same; runs the copy-and-swap operator=. What happens to a?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It gets a copy of its own values in a new array, and its old array is freed by the copy's destructor

    Copy-and-swap needs no special test for a = a: it copies, swaps and frees the old array, leaving the same values. Rule C.62 of the C++ Core Guidelines asks for exactly that safety.

  2. Question 2 of 2 In the copy-and-swap operator=, new int[] throws while Buffer copy{other}; is being made. What state is *this in?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Unchanged, because nothing in *this is touched before the copy is complete

    Copy first, then swap: if the copy cannot be made, the assignment fails without changing anything, which is the strong exception guarantee.

The rule of five: moves that hand the array over

Copying an array is wasted work when the source is about to disappear, such as a temporary or a local variable being returned. C++11 added two more special member functions for that case. A move takes the source’s resources and leaves the source empty, but still valid, as rule C.64 of the Core Guidelines requires:

  • The move constructor takes other’s pointer and size with std::exchange, from <utility>, which returns the old value and stores a new one: other is left with size 0 and a null pointer, which its destructor can delete safely.
  • The move assignment operator moves other into a temporary and swaps with it, so the old array of *this is freed by the temporary’s destructor.

std::move(b) itself moves nothing. It marks b as an object whose resources may be taken, so the compiler chooses the move constructor instead of the copy constructor; the module on move semantics explains the rules behind it. Built with AddressSanitizer again, the program has nothing to report:

Copy, self-assignment, move and reuse, with no report Python · rule_of_five_session.py
from session import run

# Built with AddressSanitizer, as before: this time it has nothing to report.
run("""
clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_five.cpp -o rule_of_five && ./rule_of_five
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_five.cpp -o rule_of_five-gcc && ./rule_of_five-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_five.cpp -o rule_of_five && ./rule_of_five
copy:  a[0] = 0, b[0] = 99
self:  a.size() = 3, a[0] = 0
move:  c[0] = 99, b.size() = 0
reuse: b.size() = 3, b[0] = 99
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_five.cpp -o rule_of_five-gcc && ./rule_of_five-gcc
copy:  a[0] = 0, b[0] = 99
self:  a.size() = 3, a[0] = 0
move:  c[0] = 99, b.size() = 0
reuse: b.size() = 3, b[0] = 99

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rule_of_five_session.py

Step through the move and the reuse of the moved-from b:

Interactive · step by step A move hands the array over Step through the move constructor and the reuse of the moved-from b. The right column holds the arrays.
A move hands the array over, step 5 of 5. Showing Buffer b (size_ 3), Buffer c (size_ 3), b's array, a new array (99 0 0, a copy). In focus: Buffer b, a new array.ownsownsBuffer bsize_ 3Buffer csize_ 3b's array99 0 0a new array99 0 0, a copy

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Before the move. b owns an array of 3 ints whose first value is 99, from the copy earlier in main.
  2. std::move(b). std::move(b) moves nothing by itself. It marks b as an object whose resources may be taken, so Buffer c = std::move(b); picks the move constructor instead of the copy constructor.
  3. The pointer changes hands. std::exchange takes b's pointer and size for c and leaves b with 0 and nullptr. No array is made and no value is copied: c[0] is 99, and b.size() is 0.
  4. Empty, but still valid. A moved-from Buffer is empty but valid, as rule C.64 asks: its destructor may run delete[] on nullptr, which does nothing.
  5. Reused. b = c; is a copy assignment into the moved-from b, which makes a new array of 3 ints for it: the program printed b.size() = 3, b[0] = 99.

The copy is independent, the self-assignment kept the values, the move left b empty, and the moved-from b could be assigned again, which is all that “valid but empty” promises.

Why the moves are noexcept

Both move operations are marked noexcept: A specifier that promises a function lets no exception out, as in Buffer(Buffer&&) noexcept. If one tries to leave such a function, std::terminate ends the program. A destructor is noexcept unless it says otherwise. More in “Destructors and RAII in C++”, and rule C.66 of the Core Guidelines asks for it. The reason is std::vector. When it runs out of room, it allocates a larger array and transfers its elements. If a transfer threw halfway, the vector could not undo it, so it moves elements only when their move constructor cannot throw, and copies them otherwise (a type that cannot be copied at all is moved anyway). The difference is easy to measure:

growth.cpp C++ · growth.cpp
// When a std::vector runs out of room, it moves its elements to a bigger array, but
// only if their move constructor is noexcept. Otherwise it copies them.
#include <cstddef>
#include <print>
#include <vector>

struct SafeMove {
    static inline int copies = 0;
    static inline int moves = 0;
    SafeMove() = default;
    SafeMove(const SafeMove&) { ++copies; }
    SafeMove(SafeMove&&) noexcept { ++moves; }
};

struct RiskyMove {
    static inline int copies = 0;
    static inline int moves = 0;
    RiskyMove() = default;
    RiskyMove(const RiskyMove&) { ++copies; }
    RiskyMove(RiskyMove&&) { ++moves; }   // not noexcept
};

int main() {
    std::vector<SafeMove> safe;
    std::vector<RiskyMove> risky;
    std::vector<std::size_t> capacities;   // each capacity safe had, in order
    for (int i = 0; i < 1000; ++i) {
        safe.emplace_back();
        risky.emplace_back();
        if (capacities.empty() || capacities.back() != safe.capacity()) capacities.push_back(safe.capacity());
    }
    std::println("capacities: {}", capacities);
    std::println("noexcept move: {} copies, {} moves", SafeMove::copies, SafeMove::moves);
    std::println("throwing move: {} copies, {} moves", RiskyMove::copies, RiskyMove::moves);
}
1,000 elements: moves with noexcept, copies without Python · growth_session.py
from session import run

# 1,000 elements added one by one, with libc++ and with libstdc++.
run("""
clang++ -std=c++23 -Wall -Wextra growth.cpp -o growth && ./growth
g++-16 -std=c++23 -Wall -Wextra growth.cpp -o growth-gcc && ./growth-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra growth.cpp -o growth && ./growth
capacities: [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
noexcept move: 0 copies, 1023 moves
throwing move: 1023 copies, 0 moves
$ g++-16 -std=c++23 -Wall -Wextra growth.cpp -o growth-gcc && ./growth-gcc
capacities: [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
noexcept move: 0 copies, 1023 moves
throwing move: 1023 copies, 0 moves

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 growth_session.py

The first line shows how both standard libraries grew the vector: its capacity doubled from 1 to 1,024, so elements were transferred 1 + 2 + 4 + … + 512 = 1,023 times in total. With a noexcept move constructor, all 1,023 transfers were moves; without it, all 1,023 were copies, and for a type like Buffer each copy allocates and fills a new array.

Key idea

Mark move operations noexcept: std::vector moves its elements when it grows only if their move constructor cannot throw, and copies every one of them otherwise.

Checkpoint

Check: Moves, and why they are noexcept

  1. Question 1 of 2 After Buffer c = std::move(b);, what does b.size() return?

    Type a number.

    A hint

    What does std::exchange leave behind?

    Show the answer: Question 1 of 2

    Answer: 0

    The move constructor takes b's size and pointer with std::exchange and leaves 0 and nullptr. The program printed move: c[0] = 99, b.size() = 0.

  2. Question 2 of 2 RiskyMove has a move constructor without noexcept. How many copies did a std::vector of RiskyMove make while 1,000 elements were added?

    Type a number.

    A hint

    The vector reallocated at 1, 2, 4 … 512 elements.

    Show the answer: Question 2 of 2

    Answer: 1023 copies

    The capacity went 1, 2, 4 … 1,024, so the reallocations moved 1 + 2 + … + 512 = 1,023 elements. Without noexcept on the move constructor, every one of them was a copy; with it, every one was a move.

Declaring one special member changes which others you get

The compiler does not always write the move operations. The standard declares an implicit move constructor and move assignment only for a class that declares no copy operation, no move operation and no destructor. Declaring a destructor, even an empty one or one written = default, is enough to lose them, and then std::move quietly falls back to the copy constructor:

lost_moves.cpp C++ · lost_moves.cpp
// Declaring a destructor, even one that does nothing, stops the compiler from
// writing the move operations. std::move then quietly copies.
#include <print>
#include <utility>

struct Payload {
    static inline int copies = 0;
    static inline int moves = 0;
    Payload() = default;
    Payload(const Payload&) { ++copies; }
    Payload(Payload&&) noexcept { ++moves; }
};

struct Plain {                  // no special member functions declared
    Payload data;
};

struct WithDestructor {
    Payload data;
    ~WithDestructor() {}        // user-declared: no implicit moves
};

struct WithDefaultedDestructor {
    Payload data;
    ~WithDefaultedDestructor() = default;   // still user-declared
};

void report(const char* name) {
    std::println("{:<24} {} copies, {} moves", name, Payload::copies, Payload::moves);
    Payload::copies = 0;
    Payload::moves = 0;
}

int main() {
    Plain p;
    Plain p2 = std::move(p);
    report("Plain");

    WithDestructor w;
    WithDestructor w2 = std::move(w);
    report("WithDestructor");

    WithDefaultedDestructor d;
    WithDefaultedDestructor d2 = std::move(d);
    report("WithDefaultedDestructor");

    (void)p2, (void)w2, (void)d2;   // not used again: keeps the compilers from warning
}
One move, then two silent copies Python · lost_moves_session.py
from session import run

# lost_moves.cpp built and run with both compilers; then each compiler is asked to
# warn about the implicit copy (-Wdeprecated for Clang, -Wdeprecated-copy-dtor for GCC).
run("""
clang++ -std=c++23 -Wall -Wextra lost_moves.cpp -o lost_moves && ./lost_moves
g++-16 -std=c++23 -Wall -Wextra lost_moves.cpp -o lost_moves-gcc && ./lost_moves-gcc
clang++ -std=c++23 -Wall -Wextra -Wdeprecated -c lost_moves.cpp 2>&1 | grep 'warning:'
g++-16 -std=c++23 -Wall -Wextra -Wdeprecated-copy-dtor -c lost_moves.cpp 2>&1 | grep 'warning:'
""")

Output

$ clang++ -std=c++23 -Wall -Wextra lost_moves.cpp -o lost_moves && ./lost_moves
Plain                    0 copies, 1 moves
WithDestructor           1 copies, 0 moves
WithDefaultedDestructor  1 copies, 0 moves
$ g++-16 -std=c++23 -Wall -Wextra lost_moves.cpp -o lost_moves-gcc && ./lost_moves-gcc
Plain                    0 copies, 1 moves
WithDestructor           1 copies, 0 moves
WithDefaultedDestructor  1 copies, 0 moves
$ clang++ -std=c++23 -Wall -Wextra -Wdeprecated -c lost_moves.cpp 2>&1 | grep 'warning:'
lost_moves.cpp:20:5: warning: definition of implicit copy constructor for 'WithDestructor' is deprecated because it has a user-provided destructor [-Wdeprecated-copy-with-user-provided-dtor]
lost_moves.cpp:25:5: warning: definition of implicit copy constructor for 'WithDefaultedDestructor' is deprecated because it has a user-declared destructor [-Wdeprecated-copy-with-dtor]
$ g++-16 -std=c++23 -Wall -Wextra -Wdeprecated-copy-dtor -c lost_moves.cpp 2>&1 | grep 'warning:'
lost_moves.cpp:40:36: warning: implicitly-declared 'WithDestructor::WithDestructor(const WithDestructor&)' is deprecated [-Wdeprecated-copy-dtor]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lost_moves_session.py

Plain moves its member. WithDestructor and WithDefaultedDestructor copy it, and with -Wall -Wextra neither compiler says a word. The implicit copy that they fall back on is deprecated in such classes, and each compiler has an option to report it: Clang’s -Wdeprecated flags both classes, while GCC’s -Wdeprecated-copy-dtor flags only the class whose destructor has a body. A logging line added to a destructor can therefore make a class slower without any warning, because every move became a copy.

What the compiler writes for the copies (copy constructor, copy assignment) and the moves (move constructor, move assignment)
Criterion CopiesMoves
You declare none of the five Written for youWritten for you
You declare a destructor Written, but deprecatedNot declared: they copy
You declare one copy The other written, deprecatedNot declared: they copy
You declare one move Deleted: move-onlyThe other not declared

A destructor added only to log a line

An empty ~WithDestructor() {}, or one that only logs, removes the implicit moves, and every std::move of the class silently becomes a copy. Rule C.21 of the Core Guidelines turns the table into one habit: if you define or = delete any of the five, define or = delete all of them. = default asks for the compiler’s version on purpose, so declare the copy and move operations = default next to such a destructor and the moves stay fast.

Checkpoint

Check: Which special members the compiler declares

  1. Question 1 of 2 WithDefaultedDestructor declares only ~WithDefaultedDestructor() = default;. What did std::move do with its Payload member?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It copied it, 1 copy and 0 moves

    Plain moved its member; WithDestructor and WithDefaultedDestructor copied theirs, and with -Wall -Wextra neither compiler said a word.

  2. Question 2 of 2 Which option reported both WithDestructor and WithDefaultedDestructor?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Clang's -Wdeprecated

    The implicit copy that such classes fall back on is deprecated, and Clang's -Wdeprecated reports it for any user-declared destructor; GCC's -Wdeprecated-copy-dtor only when the destructor is user-provided.

The rule of zero: let the members do it

The simplest way to get all five right is to write none of them. If every member manages itself, the compiler’s member-by-member versions are correct, and rule C.20 of the Core Guidelines says to rely on them:

rule_of_zero.cpp C++ · rule_of_zero.cpp
// The same Buffer built from a standard type: no special member functions at all,
// and copying, moving and destroying are all correct.
#include <cstddef>
#include <memory>
#include <print>
#include <type_traits>
#include <utility>
#include <vector>

class Buffer {
public:
    explicit Buffer(std::size_t size) : data_(size) {}   // size zeros

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

private:
    std::vector<int> data_;   // owns, copies and frees the array
};

class Owner {   // owns one int through std::unique_ptr: movable, not copyable
public:
    explicit Owner(int value) : value_{std::make_unique<int>(value)} {}
    int value() const { return *value_; }

private:
    std::unique_ptr<int> value_;
};

int main() {
    Buffer a{3};
    Buffer b = a;
    b[0] = 99;
    std::println("copy:  a[0] = {}, b[0] = {}", a[0], b[0]);

    Buffer c = std::move(b);
    std::println("move:  c[0] = {}, c.size() = {}", c[0], c.size());

    std::println("Buffer copyable: {}, movable: {}", std::is_copy_constructible_v<Buffer>,
                 std::is_nothrow_move_constructible_v<Buffer>);
    std::println("Owner copyable: {}, movable: {}", std::is_copy_constructible_v<Owner>,
                 std::is_nothrow_move_constructible_v<Owner>);

    Owner first{7};
    Owner second = std::move(first);
    std::println("second.value() = {}", second.value());
}
Correct copies and moves from the members alone Python · rule_of_zero_session.py
from session import run

# Built with AddressSanitizer: nothing to report, and no special member function written.
run("""
clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_zero.cpp -o rule_of_zero && ./rule_of_zero
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_zero.cpp -o rule_of_zero-gcc && ./rule_of_zero-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_zero.cpp -o rule_of_zero && ./rule_of_zero
copy:  a[0] = 0, b[0] = 99
move:  c[0] = 99, c.size() = 3
Buffer copyable: true, movable: true
Owner copyable: false, movable: true
second.value() = 7
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address rule_of_zero.cpp -o rule_of_zero-gcc && ./rule_of_zero-gcc
copy:  a[0] = 0, b[0] = 99
move:  c[0] = 99, c.size() = 3
Buffer copyable: true, movable: true
Owner copyable: false, movable: true
second.value() = 7

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rule_of_zero_session.py

This Buffer holds a std::vector<int>, which copies its elements, moves its array and frees it, so the class needs no special member function at all, and AddressSanitizer finds nothing. Owner holds a std::unique_ptr, which can be moved but not copied, and the compiler draws the conclusion by itself: Owner’s copy constructor is deleted and its move is noexcept, as the type traits print. Read the lesson’s three classes side by side:

What each class of this lesson writes, by the member that holds its data
Criterion Written by handWhat the class can do
Buffer with int* All fiveOnly what the five allow
Buffer with vector<int> NoneCopy and move
Owner with unique_ptr<int> NoneMove, never copy

A raw pointer member is the case the rules of three and five exist for; standard members make it unnecessary. Write the five by hand only in a class whose job is to manage one resource, the way std::vector manages an array, and keep that class small. Every other class then holds such a manager as a member and follows the rule of zero.

Checkpoint

Check: The rule of zero

  1. Question 1 of 2 Owner holds a std::unique_ptr<int> and declares no special member function. What did the type traits print for it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Owner copyable: false, movable: true

    The compiler draws the conclusion from the members: a member that cannot be copied makes the class non-copyable, and a member that moves without throwing gives the class a noexcept move.

  2. Question 2 of 2 Why did AddressSanitizer find nothing in rule_of_zero.cpp, where Buffer writes none of the five?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Buffer's std::vector member copies its elements, moves its array and frees it, so the compiler's member-by-member versions are right

    When every member manages itself, the compiler's copy, move and destructor are correct, which is rule C.20 of the C++ Core Guidelines.

Practise: IntArray, by hand and then with zero

IntArray is a fixed-size array of int on the heap, with a destructor and nothing else. Its tests check deep copies, self-assignment, moves and noexcept, and they run with AddressSanitizer:

The starter code and its sample tests

C header · int_array.h

// Starter code for "Your turn": give IntArray its copy and move operations.
#pragma once
#include <algorithm>
#include <cstddef>
#include <utility>

// A fixed-size array of ints on the heap: IntArray{4} holds four zeros.
class IntArray {
public:
    explicit IntArray(std::size_t size) : size_{size}, data_{new int[size]{}} {}
    ~IntArray() { delete[] data_; }

    // TODO: the copy constructor, the copy assignment operator, the move constructor and
    // the move assignment operator. Until you write them, the compiler's versions copy
    // the pointer, and two IntArrays delete the same array.

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

private:
    std::size_t size_;
    int* data_;
};

C++ · int_array_tests.cpp

// Sample tests for int_array.h, built with -fsanitize=address. Each test has its own
// scope, so a double free shows up as soon as the test that causes it ends.
#include <cstdio>
#include <print>
#include <type_traits>
#include <utility>
#include "int_array.h"

int passed = 0;
int failed = 0;

void expect(bool ok, const char* what) {
    if (ok) {
        ++passed;
    } else {
        ++failed;
        std::println("FAIL {}", what);
        std::fflush(stdout);   // print now, in case the program is stopped later
    }
}

IntArray filled(std::size_t size, int first) {
    IntArray a{size};
    for (std::size_t i = 0; i < size; ++i) a[i] = first + static_cast<int>(i);
    return a;
}

void copy_constructor() {
    IntArray a = filled(3, 1);
    IntArray b = a;
    b[0] = 99;
    expect(b.data() != a.data(), "a copy has its own array");
    expect(a[0] == 1 && b[0] == 99 && b[2] == 3, "writing to the copy leaves the original alone");
}

void copy_assignment() {
    IntArray a = filled(2, 5);
    IntArray b = filled(4, 0);
    b = a;
    a[0] = 0;
    expect(b.size() == 2 && b[0] == 5 && b[1] == 6, "assignment copies the size and the values");

    const IntArray& same = b;
    b = same;
    expect(b.size() == 2 && b[1] == 6, "self-assignment keeps the values");
}

void move_constructor() {
    IntArray a = filled(3, 7);
    const int* array = a.data();
    IntArray b = std::move(a);
    expect(b.data() == array && b[2] == 9, "a move takes the array instead of copying it");
    expect(a.size() == 0 && a.data() == nullptr, "a moved-from IntArray is empty");
}

void move_assignment() {
    IntArray a = filled(3, 1);
    IntArray b = filled(5, 0);
    b = std::move(a);
    expect(b.size() == 3 && b[0] == 1, "move assignment takes the values");
    expect(a.size() == 0 && a.data() == nullptr, "the source is empty after move assignment");
}

int main() {
    copy_constructor();
    copy_assignment();
    move_constructor();
    move_assignment();
    expect(std::is_nothrow_move_constructible_v<IntArray>, "the move constructor is noexcept");
    expect(std::is_nothrow_move_assignable_v<IntArray>, "move assignment is noexcept");
    std::println("{} of {} checks passed", passed, passed + failed);
    return failed == 0 ? 0 : 1;
}
The sample tests against the starter code Python · int_array_session.py
from session import run

# The sample tests against the starter code, with AddressSanitizer. Its report goes to
# a file, and grep shows the summary line.
run("""
clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address int_array_tests.cpp -o int_array_tests
ASAN_OPTIONS=abort_on_error=0 ./int_array_tests 2> asan.txt
grep SUMMARY asan.txt
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address int_array_tests.cpp -o int_array_tests-gcc
ASAN_OPTIONS=abort_on_error=0 ./int_array_tests-gcc 2> asan-gcc.txt
grep SUMMARY asan-gcc.txt
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address int_array_tests.cpp -o int_array_tests
$ ASAN_OPTIONS=abort_on_error=0 ./int_array_tests 2> asan.txt
FAIL a copy has its own array
FAIL writing to the copy leaves the original alone
[exit status 1]
$ grep SUMMARY asan.txt
SUMMARY: AddressSanitizer: double-free int_array.h:11 in IntArray::~IntArray()
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address int_array_tests.cpp -o int_array_tests-gcc
$ ASAN_OPTIONS=abort_on_error=0 ./int_array_tests-gcc 2> asan-gcc.txt
FAIL a copy has its own array
FAIL writing to the copy leaves the original alone
[exit status 1]
$ grep SUMMARY asan-gcc.txt
SUMMARY: AddressSanitizer: double-free int_array.h:11 in IntArray::~IntArray()

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 int_array_session.py

The starter fails the first test and then dies: the compiler’s copy constructor made two IntArrays share one array, and the sanitizer stops the program when the second one deletes it. Add the four missing functions to int_array.h until both compilers print 10 of 10 checks passed and grep finds no SUMMARY line:

  1. A copy constructor that allocates new int[other.size_] and copies the values with std::copy.
  2. A copy assignment operator built with copy-and-swap. Add a swap member that swaps size_ and data_.
  3. A noexcept move constructor that takes the pointer and the size with std::exchange, leaving the source empty.
  4. A noexcept move assignment operator that moves the source into a temporary and swaps with it.

Then do the opposite exercise: replace int* data_ with a std::vector<int>, delete the destructor and the four functions you wrote, and run the tests again. With both standard libraries all 10 checks still pass, including the ones that compare data() pointers, because a moved vector hands its array over instead of copying it.

Afterwards, see which of the five ideas you can apply to classes you have not met here. The questions come back to the idea you got wrong most often, from the shared array to the class that writes nothing, and every choice says why:

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 What does Holder b = a; do?

    Read the code, then choose one answer.

    class Holder {
    public:
        Holder() : data_{new int[4]{}} {}
        ~Holder() { delete[] data_; }
    private:
        int* data_;
    };
    Show the answer: Question 1 of 11

    Answer: It copies the pointer, so a and b delete the same array when they are destroyed

    The compiler writes a copy constructor that copies each member, and the member is a pointer. Both objects then own one array, and the second delete[] is a double free, which AddressSanitizer reports.

  2. Question 2 of 11 Built without AddressSanitizer, what does the second delete[] of shallow_copy.cpp do?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: It is undefined behaviour: it may crash there, crash later in unrelated code, or appear to work

    A double free is undefined behaviour in every build. A program that seems to work today may fail elsewhere tomorrow, which is why the lesson builds with -fsanitize=address.

  3. Question 3 of 11 Which three functions does the rule of three name?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: The destructor, the copy constructor and the copy assignment operator

    They come as a set: a hand-written destructor means the class owns something, and then the compiler's copies, which share it, are wrong.

  4. Question 4 of 11 What does copy-and-swap give a copy assignment operator?

    Choose every answer that is right.

    Show the answer: Question 4 of 11

    Answer:

    • The strong exception guarantee, because *this changes only after the copy succeeded
    • Safety for self-assignment without a special test

    The operator first makes a complete copy, then swaps it in, and the copy's destructor frees the old array. If the copy throws, *this is untouched; a = a copies and swaps the same values. It still allocates, once per assignment.

  5. Question 5 of 11 What does std::move(b) itself do?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: It moves nothing; it marks b as an object whose resources may be taken, so the move constructor is chosen

    std::move is a request, not an action. The module on move semantics explains the rules behind it.

  6. Question 6 of 11 A vector grows by doubling from capacity 1 while 1,000 elements are added. How many element transfers does it make in total while reallocating?

    Type a number.

    Show the answer: Question 6 of 11

    Answer: 1023 transfers

    The vector reallocates when it holds 1, 2, 4, …, 512 elements and transfers them each time: 1 + 2 + 4 + … + 512 = 1,023. The lesson measured exactly that with both libraries: all moves when the move constructor is noexcept, all copies when it is not.

  7. Question 7 of 11 A std::vector<Buffer>, with the lesson's noexcept move constructor, reallocates from 512 to 1,024 elements. How many int arrays do the 512 transfers allocate?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: None; each move hands an existing array's pointer to the new element

    This is where noexcept pays: with it, growth moves pointers; without it, every transfer copies, and for Buffer each copy allocates and fills a new array.

  8. Question 8 of 11 A class with a std::string member gains ~Session() = default;. What happens to Session s2 = std::move(s1);?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: It copies the string, because a user-declared destructor stops the compiler from declaring the moves

    Any user-declared destructor, even one written = default, means no implicit move constructor or move assignment. std::move then picks the copy constructor, without a warning under -Wall -Wextra.

  9. Question 9 of 11 A class declares only a move constructor. What happens to its copy constructor?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: It is deleted, so the class can be moved but not copied

    Declaring one special member changes which others you get, which is why rule C.21 asks you to define or delete all five once you declare any.

  10. Question 10 of 11 A class holds only a std::string and a std::vector<int>. Which special member functions should you write?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: None of them (the rule of zero)

    Both members copy, move and free themselves correctly, so the compiler's member-by-member versions are right. Writing them by hand would only add places for mistakes.

  11. Question 11 of 11 A class holds a std::unique_ptr member and declares no special member functions. What can you do with it?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: Move it, but not copy it

    unique_ptr cannot be copied, so the compiler defines the class's copy constructor as deleted, and it can be moved, so the implicit move constructor works and is noexcept. The rule of zero gives a move-only class with nothing written.

Interview questions

Fresher · What are the rule of zero, the rule of three and the rule of five?

They are three ways to get a class’s copy, move and destruction right. The rule of three, from before C++11, says that a class needing a hand-written destructor, usually because it owns a raw resource such as memory from new[], also needs a hand-written copy constructor and copy assignment operator, because the compiler’s versions copy the pointer and lead to a double free. The rule of five adds the move constructor and move assignment of C++11: declaring any of the five suppresses or deprecates some of the others, so write all five, or = default and = delete them explicitly. The rule of zero is the preferred design: build classes from members that manage themselves, such as std::vector, std::string and std::unique_ptr, and write none of the five, because the compiler’s versions are then correct.

Expect two follow-ups that test whether you can use the rules: a destructor added only for logging, and a class that should move but never copy. Decide both:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 Follow-up: a class that holds a std::string needs a destructor only to log a line. How does it keep fast moves?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Declare the copy constructor, copy assignment, move constructor and move assignment = default as well

    If you define or = delete any of the five, define or = delete all of them (rule C.21). Defaulting the other four keeps the logging destructor and the fast moves.

  2. Question 2 of 2 Follow-up: a class holds a std::unique_ptr<Engine> and a std::string. Which special member functions do you write to make it movable but not copyable?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: None; the members already make it move-only

    Owner in rule_of_zero.cpp is exactly this case: copyable false, movable true, with nothing written.

Copy control decides how a class behaves inside containers and class hierarchies, and the next two questions take one case of each: images in a vector that crash with a double free, and a polymorphic base class that copies where it should move:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    After a refactor that stores Image objects in a std::vector<Image>, the program crashes with a double free. Image holds a raw unsigned char* from new[] and deletes it in its destructor. Diagnose the crash and give two fixes, with their trade-offs.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A Shape base class declares only virtual ~Shape() = default; and a name string. Growing a std::vector<Circle> copies every element, and Shape s = circle; compiles and loses the circle's data. Explain both problems and write the special member functions the base class should declare.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The compiler writes the five special member functions member by member, so a raw owning pointer is copied shallowly and later deleted twice, which AddressSanitizer reports with all three stacks.
  • A class that manages a raw resource needs deep copies, and copy-and-swap gives its copy assignment the strong exception guarantee and safety for self-assignment.
  • Moves hand the resource over and leave the source empty but valid, and they must be noexcept: a vector of 1,000 elements made 1,023 moves with it and 1,023 copies without.
  • Declaring a destructor, even = default, removes the implicit moves, so std::move silently copies unless the four others are declared too.
  • The rule of zero, members that manage themselves and none of the five written, is the default design; the five by hand belong in a small class that manages one resource.

Terms in this lesson

copy-and-swap
A way to write copy assignment: make a complete copy of the source, swap its members with this object's, and let the copy's destructor free the old resource. It needs no test for self-assignment and changes nothing if the copy throws.
deep copy
A copy that duplicates the resource itself, as a copy constructor does when it allocates a new array and copies the values into it, so the two objects own separate resources.
rule of five
The rule of three plus the two move operations of C++11: a class that declares any of the destructor, the copy operations or the move operations defines or deletes all five, since declaring one changes the others.
rule of three
A class that needs its own destructor, usually because it owns a raw resource, also needs its own copy constructor and copy assignment operator, because the compiler's versions would share the resource.
rule of zero
Build a class from members that manage themselves, such as std::vector, std::string and std::unique_ptr, and declare none of the five copy, move and destruction functions: the compiler's member-by-member versions are then correct.
shallow copy
A copy that duplicates a pointer but not what it points to, so two objects share one resource. The compiler's copy of a class that owns memory through a raw pointer is one, and both copies later delete that memory.
special member function
One of the member functions the compiler can write for a class by itself: the default constructor, the destructor, the copy constructor, copy assignment, the move constructor and move assignment. The compiler's versions work member by member.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.