Skip to content
VLSI Mentor

C++ Part I · Module 12 · Constructors and Destructors

Delegating Constructors

Chapter 12.2 arrived at a shape: one constructor does the checking, and the other legitimate ways of supplying the data become named functions that route through it. That worked because a static function can build and return an object, so it could simply call the checking constructor and hand back the result. A constructor cannot do that. It is not making a new object, it is making this one, so calling another constructor the ordinary way would construct a separate temporary and throw it away. Before C++11 the usual workarounds were a private initialize helper that every constructor called — which puts the real work after the members already exist, with all of Chapter 12.3's consequences — or copying the invariant check into each constructor, which leaves several places for it to drift apart. C++11 added a syntax that does the obvious thing instead: one constructor names another in its initializer list and that constructor runs, on this object, to completion. The chapter measures the order, because there is one part of it people guess wrong, and it confirms the property the whole design exists for — that a check written once in the target still fires when the caller arrived through a delegation.

Intermediate15 min readC++Part IFundamentalsConstructors

Part I · Module 12 · Chapter 12.6 · Delegating Constructors

1. The Problem

Chapter 12.2 §6 established that a type should have one constructor that establishes the invariant. But a type often has several constructors, and each of them needs the invariant established.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Field(int offset, int width) { /* the check */ }
    explicit Field(int offset)   { /* ...the same check? */ }
    Field()                      { /* ...and again? */ }

Copying the check three times means three places for it to drift. And calling Field(offset, 1) the ordinary way from inside a constructor does not help — that constructs a separate temporary Field and discards it, leaving the object you were building untouched.

2. The Syntax

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    explicit Field(int offset) : Field(offset, 1) { /* body */ }
    //                          ^^^^^^^^^^^^^^^^ delegate to another constructor

In the initializer list, instead of naming members, name the class itself. That constructor then runs on this object.

Available since C++11, and Chapter 1.3 §3 fixed this curriculum at C++17, so it is simply available.

3. The Order, Measured

Azvya Education Pvt. Ltd.VLSI Mentor
deleg.cpp — the type
class Field {
public:
    // THE one that checks
    Field(int offset, int width) : offset_(offset), width_(width)
    {
        std::printf("    checking ctor(%d,%d)\n", offset, width);
        if (offset < 0 || width <= 0 || offset + width > 32)
            throw std::invalid_argument("invalid field");
    }
    explicit Field(int offset) : Field(offset, 1) { std::printf("    Field(offset) body\n"); }
    Field()                    : Field(0, 1)     { std::printf("    Field() body\n"); }
private:
    int offset_; int width_;
};

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Field(8,4):
    checking ctor(8,4)
Field(8):
    checking ctor(8,1)
    Field(offset) body
Field():
    checking ctor(0,1)
    Field() body
values: 8/4 8/1 0/1

Four facts.

The target constructor runs first, and completely. For Field(8), checking ctor(8,1) printed before anything else — members initialized, body run, invariant checked.

Then the delegating constructor's body runs. Field(offset) body printed second. So a delegating constructor's body is a post-processing step, after the object is already fully built and valid.

The direct call runs one constructor. Field(8,4) printed one line; no delegation occurred.

And the object is correct either way — 8/4, 8/1, 0/1.

4. A Delegating Constructor Cannot Also Initialize Members

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Field(int offset) : Field(offset, 1), width_(1) { }     // ?

That is not allowed, and the reason follows from Chapter 12.1 §3: the target constructor initializes all the members. By the time it returns, they exist. A member initializer in the delegating constructor would be trying to initialize something that is already initialized.

So the rule is: the delegation is the whole initializer list. Anything else the delegating constructor wants to do goes in its body — where it is assigning to already-built members, with all of Chapter 12.3's caveats, including that a const or reference member cannot be touched there at all.

Which gives a practical guideline: delegate to the constructor that takes the most information, so that the target can initialize everything and the delegating bodies have nothing left to do. In §3's Field, both delegating bodies exist only to print.

5. The Check Fires Through a Delegation

The property the whole design exists for, measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
and the check still fires through a delegation:
    checking ctor(40,1)
  Field(40) threw: invalid field

Field(40) delegated to Field(40, 1), which checked, which threw. The exception propagated out of the delegating constructor and out of the construction entirely — and per Chapter 12.2 §8, the object never existed.

An invariant written once in the target constructor protects every constructor that delegates to it.

That is Chapter 11.5 §3's first requirement — the invariant holds at construction — satisfied for all three constructors by one piece of code.

6. The Drift, Measured

Section 5 argued that a copied check drifts. Here it is, rather than asserted.

Two versions of the same type with the same three constructors — one with the check written out in each, one delegating. Then the thing that always happens: the invariant changed. The register grew from 32 bits to 64, and in the copied version only one of the three checks was updated, because the person making the change found the one the failing test pointed at.

Azvya Education Pvt. Ltd.VLSI Mentor
drift.cpp — the two classes, abbreviated
class Before {
public:
    Before(int o, int w) : o_(o), w_(w)
    { if (o < 0 || w <= 0 || o + w > 64) throw std::invalid_argument("A"); }   // updated
    explicit Before(int o) : o_(o), w_(1)
    { if (o < 0 || o + 1 > 32) throw std::invalid_argument("B"); }             // NOT updated
    Before() : o_(0), w_(1)
    { if (0 + 1 > 32) throw std::invalid_argument("C"); }                      // NOT updated
};

class After {
public:
    After(int o, int w) : o_(o), w_(w)
    { if (o < 0 || w <= 0 || o + w > 64) throw std::invalid_argument("invalid"); }
    explicit After(int o) : After(o, 1) {}
    After()              : After(0, 1) {}
};

All three calls below describe valid fields in a 64-bit register. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the register is now 64 bits wide; all three calls are valid

  Before (check copied 3x, one copy updated):
    (40, 8)  -> 48 bits    ACCEPTED
    (40)     -> bit 40     REJECTED
    (63)     -> bit 63     REJECTED
  After  (one check, delegated):
    (40, 8)  -> 48 bits    ACCEPTED
    (40)     -> bit 40     ACCEPTED
    (63)     -> bit 63     ACCEPTED

The copied version rejects two of three valid fields. Not accepts invalid ones — rejects valid ones, which is the direction that is harder to notice, because the failure arrives as "this field description doesn't work" rather than as corrupted data, and it only happens for fields above bit 31.

Three things to take from it.

The failure is partial, which is what makes it expensive. Before(40, 8) works. If your tests use the two-argument form — and they probably do, because it is the canonical one — everything passes. The single-bit form fails only for the upper half of the register.

It is not a careless mistake. Whoever updated the 64 did so because something told them to. Nothing told them about the other two, and the compiler cannot: three correct-looking if statements with different bounds is not an error in any sense a compiler can detect. Chapter 10.8 measured the same shape for resource release — correct code broken by an edit that did not touch it, zero warnings.

And the delegated version needed no edit at all beyond the one. That is the actual benefit: not fewer lines today, but that the number of places to change is one, forever.

7. Delegation or a Named Factory?

Chapter 12.2 §6 solved a related problem with static factories, and the two tools are not interchangeable. Honest comparison:

Delegating constructorNamed factory (Chapter 12.2 §6)
Signature can collide with anotherno — it is a constructoryes — fromBits(4,6) coexists with Field(4,3)
Call site names the meaningno — Field(8)yes — Field::singleBit(8)
Can validate before constructingnoyes — measured in 12.2 §7
Usable where a constructor is requiredyesno

So: delegate when the alternative is genuinely the same construction with fewer arguments — a default, an omitted value. Use a factory when the arguments mean something different, which is when the name earns its place, or when the signature would collide.

And they combine: Chapter 12.2's fromBits is a factory that calls the checking constructor, and Field() here is a delegating constructor. Both route through one check, which is the point of either.

8. A Cycle Is a Compile Error

Delegation is a graph, and nothing in the syntax stops you drawing a loop in it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class C {
    public:
        C()             : C(1) {}      // delegates to C(int)
        explicit C(int) : C()   {}     // ...which delegates back
    };

If this were permitted it would recurse forever, with no base case and no stack frame reclaimed — Chapter 10.2 §5 computed that as roughly 130,000 levels before the process dies. The compiler catches it instead:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: constructor for 'C' creates a delegation cycle [-Wdelegating-ctor-cycles]

Three things worth noting.

It is an error, not a warning. Unlike Chapter 12.8's -Wreorder-ctor, this one cannot be carried in a codebase. The diagnostic has a warning-style name because the underlying check is a warning flag that defaults to an error.

The cycle can be longer than two. A delegating to B delegating to C delegating back to A is the same error; the compiler follows the chain.

And it tells you something about the design. A cycle means you could not decide which constructor is the authoritative one. Section 5's rule resolves it: exactly one constructor checks the invariant and initializes every member, and every other constructor delegates to it, directly or through a chain that terminates there. A design that satisfies that rule cannot contain a cycle, because the chain always flows toward the one target.

That gives a practical way to check a class with several constructors without tracing the graph by hand: find the one that initializes all the members, and confirm every other constructor's delegation leads to it. If two constructors both look authoritative, you have either duplicated the invariant — Section 6's measured drift — or built a cycle.

9. Two Things It Is Not

It is not an init() helper. The old pattern — every constructor calling a private void init(...) — runs after the members exist, so it assigns rather than initializes, and it cannot touch const or reference members at all (Chapter 12.3 §5 and §6). Delegation does the real initialization.

And it is not inheriting constructors. using Base::Base; is a different feature that brings a base class's constructors into a derived class, and it belongs to Module 16. The two are easy to confuse because both reduce constructor duplication; this one delegates within a single class.

10. What to Actually Do

Write one constructor that establishes the invariant, and delegate every other constructor to it.

Delegate to the constructor taking the most information, so the target initializes everything.

Keep delegating bodies empty or nearly so. Anything there is assignment, not initialization.

Use a factory instead when the arguments mean something different or the signature would collide (Chapter 12.2 §6).

Combine them — a factory that checks its own precondition and then calls the checking constructor.

Never copy the invariant check into several constructors.

Do not use a private init() helper — it assigns, and cannot touch const or reference members.

And still ask whether each constructor deserves to exist. Delegation makes them cheap, not free.

11. Mistakes Worth Making Once

Copying the invariant check into every constructor. It stays identical until someone edits one.

Calling another constructor like a function — Field(offset, 1); inside a body builds and discards a temporary.

Mixing a delegation with member initializers. Not allowed; the target initializes everything.

Putting real initialization in the delegating body. It is assignment, with Chapter 12.3's three impossibilities.

Delegating to the constructor with the fewest arguments. Then the body has work to do, and the work is assignment.

Using a private init() instead. The pre-C++11 workaround, with the same flaw.

Confusing it with using Base::Base; — a different feature, Module 16's.

Assuming the delegating body runs first. Measured: the target completes first.

12. Interview Relevance

"How do you avoid duplicating logic across constructors?" Delegate: one constructor names another in its initializer list, and that one runs to completion first. The strong answer adds why — so the invariant is written in exactly one place.

"What is the order of events?" The target constructor runs completely — members, body, checks — and then the delegating constructor's body runs. Measured, and people frequently guess the reverse.

"Can a delegating constructor also initialize members?" No, because the target already initialized them all. This follows from Chapter 12.1's ordering and is a good test of whether someone understands it.

"How is it different from a private init() function?" init() runs after members exist, so it assigns rather than initializes and cannot handle const or reference members. Delegation does the real initialization.

"When would you use a static factory instead?" When the signature would collide, when the arguments mean something different, or when you need to validate before constructing anything.

13. What You Should Be Able to Say Now

  1. A constructor delegates by naming its own class in the initializer list — C++11, so available throughout this curriculum.
  2. Measured: the target runs first and completely — checking ctor(8,1) before Field(offset) body.
  3. Then the delegating constructor's body runs — it is post-processing on a fully built object.
  4. A delegating constructor cannot also initialize members, because the target initialized all of them.
  5. So delegate to the constructor taking the most information and keep the body empty.
  6. Measured: the check fires through a delegation — Field(40) threw invalid field via checking ctor(40,1).
  7. One invariant, written once, protects every delegating constructor (Chapter 11.5 §3).
  8. Calling a constructor like a function builds a temporary and discards it.
  9. A factory can do what delegation cannot — collide signatures, name the meaning, validate first.
  10. The two combine: a factory that checks its precondition and then delegates.
  11. It is not an init() helper — that assigns and cannot touch const or reference members.
  12. It is not using Base::Base; — that is Module 16's inheriting constructors.
  13. Delegation makes constructors cheap to maintain, not automatically worth having.

14. Exercises

Exercise 1 — delegate it. Given Counter(int start, int limit) with a check, add Counter(int limit) starting at 0 and Counter() with limit 100, by delegation.

Exercise 2 — predict the order. For C() : C(1) { std::printf("A\n"); } and C(int) : C(1,2) { std::printf("B\n"); } and C(int,int) { std::printf("C\n"); }, give the output of C c;.

Exercise 3 — why not both? Explain why Field(int o) : Field(o, 1), width_(1) {} is rejected, using Chapter 12.1 §3.

Exercise 4 — which direction? A type has T(int) and T(int, int). Say which should delegate to which and why.

Exercise 5 — delegation or factory? Choose for each: (a) a default-valued version of the same construction (b) a bit-range form of a field (c) a version that parses a string (d) a single-bit field.

Exercise 6 — the drift. Describe concretely what goes wrong six months after copying an invariant check into three constructors.

Exercise 7 — replace the helper. A class has three constructors all calling void init(int, int). Rewrite with delegation and say which members could now be made const.

15. Summary

  • A constructor delegates by naming its class in the initializer list (C++11).
  • Measured: the target runs to completion first, then the delegating body — which is post-processing.
  • A delegating constructor cannot initialize members too, so delegate to the one taking the most information.
  • Measured: the invariant check fires through a delegation and the object never exists.
  • One check protects every constructor that delegates to it.
  • Calling a constructor like a function makes and discards a temporary.
  • A factory does what delegation cannot — collide signatures, name the arguments' meaning, validate first; and the two combine.
  • It is not a private init() (which assigns) nor using Base::Base; (Module 16).
  • Never copy an invariant check; it stays identical only until someone edits one copy.

Six chapters have now been spent on bringing an object into existence, and the module's organising claim was about two events rather than one. Everything so far has had a counterpart on the other side that has been visible in the traces and never discussed: Chapter 12.1's measurement showed a destructor body running before its members were destroyed, Chapter 12.2's throwing constructor showed destructors running for members that had been built, and every trace in this module has ended with objects being taken apart in an order nobody specified. The next chapter is that side properly — what a destructor is for, when exactly it runs for each of the storage durations, and the one case where it does not run at all despite the object having existed. It is rated Critical because the guarantee it provides is the one the two chapters after it are built on.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.