Skip to content

Module 10 — Self-Assessment

Ten questions. Part A checks that you have the vocabulary; Part B checks that you can predict — which is the only part that tells you whether you can use this material.

Answer on paper, or in a scratch file. Then mark yourself honestly, and note which section of Concepts each miss points back to.


Part A — Recall and comprehension

  1. What does the compiler-generated copy constructor do for a class with a std::string member and a std::vector<int> member? Is that correct, and why? (See section 3.)

  2. State the Rule of Three, the Rule of Five and the Rule of Zero, and say what the progression between them is telling you. (Section 6 and section 9.)

  3. What does std::move(x) do? Name its return type, and say what decides whether a move actually happens afterwards. (Section 7.)

  4. What may you legitimately do with a moved-from object? Give three operations that are guaranteed to work and one that is not. (Section 7.)

  5. Why should a move constructor be noexcept, and what exactly goes wrong if it is not? Be specific about which standard type cares and why it cares. (Section 8.)


Part B — Prediction and explanation

  1. Predict the output of this program, and then say what is wrong with it.
#include <iostream>

struct Own {
    int* p;
    explicit Own(int v) : p(new int(v)) { std::cout << "ctor\n"; }
    ~Own() { std::cout << "dtor\n"; delete p; }
};

int main() {
    Own a(1);
    Own b = a;
    *b.p = 99;
    std::cout << *a.p << '\n';
}

Answer. It prints ctor, then 99, then two dtor lines — and the second dtor is a double free. Own b = a; invoked the compiler-generated copy constructor, which copied the pointer, so both objects point at one int. Writing through b changed what a sees (that is why 99 prints). The undefined behaviour begins at the second delete; the program may crash there, or later, or not at all in this run, and "it did not crash" is not evidence.

A full-credit answer also says what the fix is: a deep copy constructor (p(new int(*other.p))), or = delete, or a Rule of Zero redesign with a std::unique_ptr<int> member — in which case the copy would have been deleted for you and this program would not have compiled.

  1. Predict the counters. A Tracked class with a noexcept move constructor and a const std::string name_ member. Predict the result of each line, then say which one is the trap.
Tracked a("a");
Tracked b = a;                    // (1)
Tracked c = std::move(a);         // (2)
Tracked& alias = b;
b = std::move(alias);             // (3)

Answer. (1) is a copy, one copy, zero moves. (2) is a move — one move, zero copies — and a is left valid but unspecified. (3) is the trap, and it has two parts: the move happens (zero copies, one move), and because the source and destination are the same object, the string member is moved into itself, which may leave b.name_ empty. It compiles, it runs, and the compiler says nothing.

The const on name_ would matter in a different line: std::move(c.name_) selects the copy constructor, because const std::string&& cannot bind to std::string&&. Both halves of the answer are the point: a const member is copied forever, and a self-move is silent.

  1. Predict what changes. A working class has a destructor, no copy operations declared, and no move operations. You add Widget(Widget&&) noexcept = default;. What does the class's interface look like before and after, and what will existing code do differently?

Answer. Before: the copy constructor and copy assignment are generated; no move operations exist, so every "move" is a copy. After: the move constructor exists, the copy constructor and copy assignment are deleted (implicitly, because a move operation was declared), and the move assignment operator still does not exist. So the class goes from "copyable and never moved" to "move-only", and any existing code that copied a Widget now fails to compile with use of deleted function. A full-credit answer adds the fix: declare whichever of the five you actually want, explicitly.

  1. Explain this measurement. The same class, twice; only the move constructor's exception specification differs. Why does the looser signature produce more copies?
Item<true>,  noexcept move:   constructions 15, copies 0, moves 7
Item<false>, throwing move:   constructions 15, copies 7, moves 0

Answer. Both versions were stored in a std::vector that reallocated three times while growing to eight elements. On reallocation the vector must transfer the seven existing elements, and it offers the strong guarantee: if the transfer throws, the vector must be unchanged. Moving elements out of the old buffer destroys the originals, so a throw part-way through would leave the vector unrecoverable; Item<false> therefore cannot be moved safely, and the vector copies the elements instead. Item<true> promises the move cannot throw, so the vector moves. Both classes behave identically and print the same results — the difference is seven copies, each of which allocates if the element owns a string, and no diagnostic anywhere.

A full-credit answer names the mechanism too: std::move_if_noexcept, which returns an rvalue reference when moving is safe and a const lvalue reference otherwise.

  1. Explain a design decision. A colleague proposes giving every class in your library a destructor, a copy constructor, a copy assignment operator, and both move operations — "so we are explicit about everything". Argue both sides, then decide.

Answer. For: the compiler's generation rules are subtle, and writing all five removes the surprise. A class with all five written out is fully predictable, and noexcept can be stated where it matters. Against: five functions per class is a great deal of code, most of it identical to what the compiler would have generated; every hand-written one is a chance to get self-assignment or the allocation ordering wrong, and the compiler's versions cannot forget a member added later — whereas a hand-written member-wise copy silently can. Writing a destructor suppresses the move operations, which is the opposite of what "being explicit" intends.

A full answer resolves it in the direction of the Rule of Zero: make the class own no resource directly, let standard members own things, and declare none of the five. Reserve hand-written special members — and explicit = default where you want the generated form with a change to the group's composition — for the few classes that genuinely own a non-memory resource. A weaker but acceptable answer is "write them all, correctly, and add static_assert tests for the properties you care about"; what makes it acceptable is that it names the maintenance cost it accepts.


Marking yourself

Score What it means
≥ 80%, every miss explained Move on to Module 11
60–79% Re-read Concepts sections 5 to 9, redo Lab 10's requirements 5 to 8, then retake in a week
< 60% Redo the double-free experiment from the start, with counters, before going further

This module is a prerequisite for everything after it. Compiling and running is not enough here: if you cannot predict the counters, the class is not understood yet — and every later module in this course builds a class that owns something.

A quiz is diagnostic, not a grade. The useful output is the list of concepts that need another look, so keep that list and check it again before the capstone.


Next: Summary — the module compressed to what you must retain.