C++ / CLASSES AND OBJECT LIFECYCLE
Delegating and defaulted constructors
Chain constructors so one of them does the real initialisation, and use = default to get the compiler's own constructor back.
What you will learn
- Delegate by putting only `OtherCtor(args)` in a constructor's member-initialiser list
- Predict the run order: target constructor completes fully, then the delegating body
- Restore a suppressed default constructor with `X() = default;` in the class body
- Explain why `X() = default;` keeps X trivial but `X() {}` does not
Understanding Delegating and defaulted constructors
A delegating constructor puts the name of its own class in its member-initialiser list instead of member names: `Connection(std::string h) : Connection(std::move(h), 443) {}`. That entry must stand alone — you cannot delegate and also initialise a member in the same list. The reason is mechanical rather than stylistic: the target constructor already initialised every base and every member, so a second initialiser would be initialising something that is already alive.
The sequence is easy to reason about once you see it as a normal constructor call. The target constructor runs its own member-initialiser list and its own body to completion, and only then does the delegating constructor's body run. So the delegating body always sees fully built members and is the right place for a range check or an adjustment. One consequence surprises people: the moment the target finishes, the object counts as constructed, so if the delegating body throws, the destructor runs.
`= default` is the other half of the topic and solves a different problem. Declaring any constructor of your own stops the compiler generating the default constructor, so `Point p;` suddenly fails to compile; `Point() = default;` asks for the generated definition back. It is not a synonym for `Point() {}` — an explicitly defaulted constructor is not user-provided, so the class stays trivially default constructible and can be implicitly constexpr and noexcept. What it does not do is zero anything: members without a default member initialiser are default-initialised, which for `int` means an indeterminate value.
<iostream>
<string>
<utility>
class Connection {
std::string host_;
int port_;
int timeoutMs_;
public:
// Target: the only constructor that touches members directly.
Connection(std::string host, int port, int timeoutMs)
: host_(std::move(host)), port_(port), timeoutMs_(timeoutMs) {
std::cout << "target ctor: " << host_ << ':' << port_
<< " timeout=" << timeoutMs_ << '\n';
}
Connection(std::string host, int port)
: Connection(std::move(host), port, 5000) {
std::cout << "two-arg body\n";
}
Connection(std::string host)
: Connection(std::move(host), 443) {
std::cout << "one-arg body\n";
}
void report() const {
std::cout << host_ << ' ' << port_ << ' ' << timeoutMs_ << '\n';
}
};
int main() {
Connection c("example.com");
c.report();
}
A constructor either initialises the bases and members itself or hands the whole job to another constructor of the same class, and `= default` hands that job back to the compiler.
Worked examples
Getting the default constructor back
Shows that writing any constructor removes the implicit default one, and that `= default` reinstates it.
<iostream>
struct Point {
int x = 0; // default member initialisers
int y = 0;
Point(int a, int b) : x(a), y(b) {}
Point() = default; // without this line, `Point p;` will not compile
};
int main() {
Point p;
Point q(3, 4);
std::cout << p.x << ',' << p.y << '\n';
std::cout << q.x << ',' << q.y << '\n';
}
Example explained
Line 1`Point(int, int)` is a user-declared constructor, so the compiler stops generating `Point()`.
Line 2`Point() = default;` asks for the generated definition instead of you hand-writing an empty body.
Line 3The `= 0` initialisers on the members are what actually produce 0,0 — the defaulted constructor just runs them.
Line 4Remove those initialisers and `p.x` becomes indeterminate even though the constructor is defaulted.
Throwing after delegation
Demonstrates that the destructor runs when a delegating constructor's body throws, because the target already completed.
<iostream>
<stdexcept>
struct Gate {
int id;
Gate(int i) : id(i) { std::cout << "target ctor " << id << '\n'; }
Gate() : Gate(7) {
std::cout << "delegating body starts\n";
throw std::runtime_error("late failure");
}
~Gate() { std::cout << "~Gate " << id << '\n'; }
};
int main() {
try {
Gate g;
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << '\n';
}
}
Example explained
Line 1`Gate() : Gate(7)` runs the target first, which prints and sets `id` to 7.
Line 2Because the target returned normally, the object is considered constructed from that point on.
Line 3The throw therefore unwinds through `~Gate`, printing before the catch block.
Line 4A non-delegating constructor that throws in its body would destroy only its members, never calling `~Gate`.
= default is not the same as an empty body
Compares an explicitly defaulted default constructor with a hand-written empty one for triviality.
<iostream>
<type_traits>
struct A { int v; A() = default; };
struct B { int v; B() {} };
int main() {
std::cout << std::boolalpha
<< std::is_trivially_default_constructible<A>::value << '\n'
<< std::is_trivially_default_constructible<B>::value << '\n';
}
Example explained
Line 1`A() = default;` is declared and defaulted in one go, so it is not user-provided and A stays trivial.
Line 2`B() {}` is user-provided even though the body is empty, which is enough to lose triviality.
Line 3Neither constructor initialises `v`; triviality is about how construction happens, not about the value produced.
Line 4Triviality is what lets `A a;` compile down to no work at all and lets A be used where trivial types are required.
Important notes
A delegation cycle such as `A() : A(0) {}` where `A(int)` delegates back to `A()` is ill-formed, but no diagnostic is guaranteed; some compilers accept it and you get infinite recursion and a stack overflow at run time.
Keep `= default` on the constructor's first declaration. Declaring `X();` in the class and defining `X::X() = default;` out of line makes it user-provided, so the type quietly loses triviality.
Common mistakes
Mixing delegation with member initialisers, as in `Connection(std::string h) : Connection(h, 443), timeoutMs_(0) {}` — this is a hard compile error, because the target has already initialised every member.
Calling the other constructor as a statement in the body, `Connection(h, 443, 5000);`, instead of in the member-initialiser list — that constructs and immediately destroys a temporary, leaving the object's own members uninitialised.
Assuming `X() = default;` zeroes the members: for `struct P { int x; P() = default; };` the value of `p.x` is indeterminate, and reading it is undefined behaviour that often looks like 0 in a debug build and like garbage in a release build.
Try it yourself
Change, predict, then run
Write a `Timer` class holding `hours`, `minutes`, `seconds` (each with a default member initialiser of 0) whose three-argument constructor is the only one that assigns members, and have `Timer(int m, int s)` and `Timer(int s)` delegate down the chain. Add `Timer() = default;`, then construct one object with each constructor and print the three values to confirm the defaults were filled in by delegation.
Open the C++ workspaceCheck your understanding
A delegating constructor's body throws an exception after its target constructor has already returned. What happens to the object being built?
- Its destructor runs, then the exception propagates out
- Only its members are destroyed; the destructor is not called
- Nothing is destroyed, since construction never completed
- The target constructor runs a second time to undo its work
Show answer
Completion of the target constructor is the point at which the object counts as fully constructed, so unwinding out of the delegating body invokes the destructor. Option 1 is tempting because it is the correct rule for an ordinary non-delegating constructor that throws in its body — there the destructor is skipped and only the already-built members and bases are destroyed — but delegation changes when the object is considered alive.