5.2.6 · D3C++ Programming

Worked examples — Constructors — default, parameterized, copy, delegating

3,599 words16 min readBack to topic

The scenario matrix

Every C++ program that builds objects lands in one of these cells. We will cover all of them.

Cell Trigger Danger / twist
A Born from nothing Point p; compiler-synthesized vs = default; what if you declared another ctor?
B Born from values Point p(3,4); explicit, implicit conversion Meter m = 5;
C Copy — POD (plain data) Point q = p; shallow copy is fine here
D Copy — owns a resource Buf b = a; shallow copy = double free; must deep-copy
E Degenerate resource Buf b(0); zero-length buffer, new int[0], empty copy
F Self-copy edge a = a; (via ctor path) does the copy path survive aliasing?
G Delegation Point()Point(0,0) the only-thing-in-list rule; cycle = UB
H Init-order trap : b(a), a(5) members init in declaration order
I Word problem model a bank account pick the right ctor per situation
J Exam twist count ctor calls pass/return by value + copy elision

Each example below is tagged with the cell(s) it clears.


Example 1 — Cell A (born from nothing) + the "I lost my default" trap

Forecast: guess before reading — does line (1) give you x=0, y=0, or does it fail?

  1. Look for a no-argument constructor. Why this step? Line Point p; needs a constructor callable with zero arguments — that is the definition of a default constructor.
  2. We declared Point(int,int). Why this matters? The moment you declare any constructor, the compiler stops synthesizing the default one. So there is no Point() available.
  3. Verdict. Line (1) is a compile error; line (2) compiles: x=3, y=4. Why? Because step 2 removed the only ctor that could serve Point p;, while Point q(3,4) exactly matches the Point(int,int) we did declare.
  4. The fix. Why? To keep both behaviours, explicitly restore the default:
Point() = default;   // brings back the synthesized default constructor
Point(int a, int b) : x(a), y(b) {}

Example 2 — Cell B (born from values) + explicit

Forecast: which of (1)(2)(3) survive the explicit keyword?

  1. explicit blocks the implicit conversion path. Why this step? explicit forbids the compiler from silently turning an int into a Meter unless you ask directly.
  2. Line (1) Meter a(5)direct-initialization, you named the type. Why OK? Direct-init is always allowed. a.m == 5.
  3. Line (2) Meter b = 5copy-initialization from an int. Why blocked? This form asks for an implicit int→Meter conversion; explicit rejects it → compile error.
  4. Line (3) Meter c = Meter(5) — you wrote Meter(5) explicitly, then copy that Meter. Why OK? The conversion is now spelled out; the copy of an existing Meter is fine. c.m == 5.

See explicit keyword and implicit conversions for the full rules.


Example 3 — Cell C (copy of plain data — shallow is fine)

Forecast: does changing p.x also change q.x?

  1. The synthesized copy ctor does member-wise copy. Why this step? For Point the members are two ints — copying an int copies its value, not a shared address.
  2. q gets its own independent x and y. Why safe? No pointer is shared; q.x was set to 3 at the copy, and p.x = 99 touches only p.
  3. Result: q.x == 3, q.y == 4, p.x == 99. Why? Because q copied p's values (3 and 4) into its own storage back when the copy happened; the later p.x = 99 writes only to p's memory, which q does not share.

Example 4 — Cell D (copy that owns a resource — the double-free)

Below: Figure 1 — left panel shows Version A (shallow copy) where a.data and b.data are two arrows pointing to one shared cyan buffer, so both destructors free the same block. The right panel shows Version B (deep copy) where each arrow points to its own amber buffer.

Figure — Constructors — default, parameterized, copy, delegating

Forecast: how many times is the same address passed to delete[]?

  1. Version A shallow-copies the pointer. Why dangerous? Look at the left panel of Figure 1: a.data and b.data hold the same address (the cyan buffer). Two owners, one buffer.
  2. Both destructors run. Why fatal? When b dies, delete[] b.data frees the buffer. When a dies, delete[] a.data frees the already-freed buffer → double free, i.e. Undefined Behaviour (UB) — the standard guarantees nothing; typically a crash or heap corruption.
  3. Version B — deep copy (the right panel of Figure 1): allocate a fresh buffer and copy element-by-element.
Buf(const Buf& o) : data(new int[o.n]), n(o.n) {
    for (int i=0;i<n;i++) data[i] = o.data[i];
}

Why this step? Now a.data != b.data (two amber buffers). Each destructor frees its own memory — no double free.

  1. Contents after deep copy: a holds [0,1,4], b holds [0,1,4] but in a separate array. Mutating b.data[0] leaves a.data[0] untouched.

Example 5 — Cell E (degenerate: zero-length buffer)

Forecast: does n = 0 crash, or is it a valid empty object?

  1. new int[0] is legal in C++. Why? It returns a valid, non-null pointer that you may safely pass to delete[]. The standard does not promise the address is distinct from every other allocation — it only guarantees the pointer is well-formed for delete[]. You may not dereference it.
  2. The copy loop for (i=0; i<0; ...) never executes. Why safe? The condition 0 < 0 is false immediately — zero iterations, no out-of-bounds read.
  3. Destruction: delete[] data on a pointer from new int[0] is well-defined. Both a and b free their own (empty) allocations.
  4. Result: a.n == 0, b.n == 0, no read/write of any element, no crash. Why? Because we sized the copy from o.n (which is 0), the allocation is empty and the loop bound is 0, so no element is ever touched — the degenerate input flows through safely instead of hitting a hardcoded size.

Example 6 — Cell F (self-referential copy safety)

Forecast: guess — is this == &o ever possible inside a copy constructor?

  1. A copy constructor builds a brand new object. Why this matters? this points to memory that did not exist a moment ago (b is being constructed). The source o is an already-living object. They are necessarily different objects.
  2. So this == &o is impossible. Why? An object cannot be its own not-yet-constructed self. Contrast with assignment a = a; where this == &other is possible and must be guarded.
  3. Consequence: the deep-copy loop data[i] = o.data[i] is always reading a different buffer than the one it writes — no aliasing hazard.
  4. Verify by construction: for Buf b(a), addresses &a != &b, and b.data != a.data after deep copy. Why? Because b occupies freshly allocated storage distinct from a, and the deep-copy ctor allocates a new inner buffer, so both the object addresses and the buffer addresses differ.

Below: Figure 2 — left panel is a legal delegation tree: Point() and Point(int) both arrow into the single amber TARGET Point(int,int). Right panel is an illegal cycle: A() arrows to A(int) which arrows back to A(), a loop that never reaches a target.

Figure — Constructors — default, parameterized, copy, delegating

Forecast: predict p, q, r's (x,y) before tracing.

  1. Point p; calls Point() → delegates to Point(0,0). Why this step? The delegate's list contains only Point(0,0); the target runs fully first, setting x=0,y=0. Result p = (0,0).
  2. Point q(7); calls Point(int) → delegates to Point(7,7). Result q = (7,7).
  3. Point r(2,3); calls the target directly. Result r = (2,3).
  4. Why can't a delegate also init members? Why the restriction? By the rule above, the target Point(int,int) is the sole place members get set. If a delegate wrote : Point(0,0), x(9), the object would be initialized twice — forbidden, so C++ only allows the single delegating call (Figure 2, left tree).
  5. The cycle (right panel). Why fatal? If A() delegates to A(int) and A(int) delegates back to A(), construction never reaches a real target → Undefined Behaviour (UB) / infinite loop. Delegation must form a tree ending at a genuine target.

Example 8 — Cell H (member init order trap)

Forecast: you might guess a=5, b=5. Look again at the declaration order.

  1. Init order = declaration order, not list order. Why? The compiler always initializes members in the order they appear in the class body, regardless of how you sequence the initializer list.
  2. b is declared first → b is initialized first, using b(a). Why is this a bug? At this instant a is not yet initialized (it's declared after b). So b is set from an indeterminate a → garbage.
  3. Then a is initialized from a(v)a = 5. Correct, but too late for b.
  4. Result: a == 5, b == <indeterminate>.

See Member Initializer Lists for the full mechanics.


Example 9 — Cell I (word problem: bank account)

Forecast: which ctor fires on each line, and what balance results?

  1. Line (1) BankAccount alice; → default → delegates to BankAccount(0.0). Why? No argument means the "born from nothing" recipe, which we routed to the target with 0.0 — one place defines "empty account" (the DRY = Don't-Repeat-Yourself principle in action). alice.bal() == 0.0.
  2. Line (2) BankAccount bob(250.0); → parameterized target directly. bob.bal() == 250.0.
  3. Line (3) BankAccount joint = bob;copy constructor (synthesized; only a double member, so member-wise copy is safe). joint.bal() == 250.0, independent of bob.
  4. Sanity (units). Balance is money; all three are non-negative doubles, matching the real-world constraint that a fresh or copied account is well-defined.

Example 10 — Cell J (exam twist: count the copy-constructor calls)

Forecast: guess a number 0–3 before tracing.

  1. Line (1) is a default construction, not a copy. Why? Tracer a; invokes Tracer(), so copies stays 0 so far.
  2. Passing a by value into byValue. Why a copy? The parameter t is a new object built from a, which invokes the copy ctor → +1 copy (copies becomes 1).
  3. return t; returns by value. Why another copy? The returned Tracer (used to build b) is a new object built from t, invoking the copy ctor again → +1 copy (copies becomes 2). This is the one copy elision / RVO usually removes.
  4. Total ignoring elision: 1 + 1 = 2 copies. Why "ignoring elision"? Modern compilers may elide the return copy (and since C++17 must elide certain temporaries), reducing observed copies to 1 — but the language model without elision counts 2.
  5. Exam-safe answer: state 2, then note "compilers may elide to 1." See Move Semantics — move constructor for how moves change this count.

Recall

Which line fails: Point p; after declaring Point(int,int) and why?
It fails — declaring any ctor removes the synthesized default; restore with Point()=default.
Difference between Point p; and Point p{} for a = defaulted Point?
Point p; (default-init) may leave int members as garbage; Point p{} (value-init) zero-initializes them to 0.
Why is a copy constructor immune to the self-assignment bug?
It builds a brand-new object, so this can never equal &other.
new int[0] — legal?
Yes: returns a valid non-null pointer safe for delete[]; the copy loop runs 0 times.
Delegating-constructor rule in one line?
Its initializer list must contain exactly one entry — the call to another ctor of the same class — and no direct member initializers.
: a(v), b(a) with b declared before a — what happens?
b is initialized first (declaration order) from an uninitialized a → garbage b.
Copies in Tracer b = byValue(a); ignoring elision?
2 (pass-by-value + return-by-value).
What does the -Wreorder flag do?
Warns when the initializer-list order differs from member declaration order (enabled via -Wreorder or -Wall).