1. Introduction
When a class becomes even a little bit useful, it suddenly turns out that constructing it “in a single way” is inconvenient. A class user (including you a week later) wants sometimes to create an object from full data, sometimes from minimal data, sometimes an “empty” placeholder object. And here you reach to write 3–4 constructors… and copy-paste begins.
Imagine you write a task class for a console scheduler (our tutorial project): a task has a title and a priority. One constructor is the “full form”, the second — “title, default priority”, the third — “empty task”. If in each of them you manually repeat the same checks and assignments — sooner or later they will diverge. This is not philosophy, this is statistics.
To feel the pain, let’s look at a “naive” variant:
#include <string>
class Task {
std::string title_;
int priority_;
public:
Task(std::string title, int priority) {
if (title.empty()) title = "untitled";
if (priority < 1) priority = 1;
if (priority > 5) priority = 5;
title_ = title;
priority_ = priority;
}
Task(std::string title) {
if (title.empty()) title = "untitled";
int priority = 3;
title_ = title;
priority_ = priority;
}
};
The code is not yet “horrible”, but it already suspiciously looks like copy-paste. Now imagine you added a third rule: “trim whitespace at the ends” or “disallow titles longer than 80”. You added it in one constructor, forgot in the second… and off it goes.
2. Delegating constructors: idea and execution order
Idea in plain language
A delegating constructor is a way to tell the compiler: “I want to create the object this way, and another constructor is just a convenient wrapper”. So one constructor becomes the main (central) one, and the others are thin “aliases” that forward the call to the main one.
It feels very similar to a good practice from regular functions: don’t spread logic across 5 places, make one function that does the work and several small functions that prepare arguments and call it. Here the same applies to constructors.
The delegation syntax looks like this:
ClassName(args_for_this_ctor) : ClassName(args_for_other_ctor) {
// body (executes AFTER the delegated constructor)
}
Yes, a constructor calls another constructor of the same class. And no, this is not recursion in the “snake biting its tail” sense — if done correctly, the chain ends at the “main” constructor which actually initializes the fields.
The main rule and execution order
When you first see delegation, it’s easy to imagine an incorrect picture: like both constructors somehow “together” initialize fields. Actually the model is simpler and stricter: exactly one constructor is responsible for creating the fields, and the other only forwards the call.
A delegating constructor has key behavior: first the constructor we delegated to runs (it initializes the object), and only then the body of the delegating constructor runs (if present).
Here is a compact diagram:
flowchart TD
A["Task(title)"] --> B[": Task(title, 3)"]
B --> C["Task(title, priority) initializes fields"]
C --> D["Body of Task(title) (if any)"]
If you remember this picture, you will avoid half of the common mistakes.
3. Examples: Rectangle and Task
Rectangle: square as a special case of rectangle
Before touching our project, let’s warm up with an example that requires no context. A rectangle has width and height. A special case is a square, where width equals height. The dumbest thing is to duplicate initialization in two constructors. The nicest is to delegate.
class Rectangle {
int w_;
int h_;
public:
Rectangle(int w, int h) : w_(w), h_(h) {}
explicit Rectangle(int side) : Rectangle(side, side) {}
};
Note two things.
First, the constructor Rectangle(int side) does not initialize w_ and h_ directly — it delegates to the “main” Rectangle(int w, int h).
Second, such code reads very well: we immediately see the author’s intent — “a square is created as a rectangle with sides side, side”. It is like a comment, but the compiler also ensures you don’t break the logic.
Practical example: the Task class for a console TaskTracker
Now we’ll make an example closer to reality: a task in our hypothetical console app TaskTracker. Even if in your course the project is named differently — that’s fine: the important thing is the idea “model + correct construction”.
Let a task have a title and a priority from 1 to 5. Our goal is to ensure any task after construction is valid: title is not empty, priority is in range.
Version without delegation
First, “how it’s often done”:
#include <string>
class Task {
std::string title_;
int priority_;
public:
Task(std::string title, int priority) {
if (title.empty()) title = "untitled";
if (priority < 1) priority = 1;
if (priority > 5) priority = 5;
title_ = title;
priority_ = priority;
}
Task(std::string title) {
if (title.empty()) title = "untitled";
int priority = 3;
title_ = title;
priority_ = priority;
}
};
The problem here is not only copy-paste. There is also a “silent” problem: fields are first created “as they come”, and then we assign them. For int this is not that bad, but for more complex types (strings, vectors) we lose part of the initialization intent: the object is already created and then we “re-dress” it.
Main constructor + delegation
Let’s make a main constructor that accepts everything, normalizes and initializes fields. The others will delegate.
#include <string>
#include <utility>
class Task {
std::string title_;
int priority_;
static int clamp_priority(int p) {
if (p < 1) return 1;
if (p > 5) return 5;
return p;
}
static std::string normalize_title(std::string t) {
if (t.empty()) return "untitled";
return t;
}
public:
Task(std::string title, int priority)
: title_(normalize_title(std::move(title)))
, priority_(clamp_priority(priority))
{}
explicit Task(std::string title)
: Task(std::move(title), 3)
{}
Task()
: Task("untitled", 3)
{}
};
Several things are pleasing here, but the most important is validity rules live in one place: in the main constructor and its small helpers. Want to change the priority range to 1..10? Change clamp_priority, and the whole application behaves consistently.
Another useful detail: delegating constructors here are “thin”. They do not try to repeat in the body what the main constructor already does. They just pick arguments.
Quick check in main()
Let’s add a small main() to see that everything works.
#include <iostream>
#include <string>
int main() {
Task a; // title="untitled", priority=3
Task b{"", 100}; // title="untitled", priority=5
Task c{"Read a book"}; // title="Read a book", priority=3
std::cout << "Tasks created.\n"; // Tasks created.
}
We do not print fields yet (we don’t have operator<< for the class — that will be in later topics), but it is already important that creation does not break and does not require a “second setup step”.
Delegating constructor body: when it is needed
In most cases a delegating constructor should be “almost empty”. But sometimes you want a small additional logic after delegation — for example, logging (for learning) or simple diagnostics.
It is important to understand: the body of a delegating constructor executes after the main constructor has run and the object is already initialized. This means you can safely access fields in the body: they already exist and are in a correct state.
For example:
#include <iostream>
#include <string>
#include <utility>
class Task {
std::string title_;
int priority_;
public:
Task(std::string title, int priority)
: title_(title.empty() ? "untitled" : std::move(title))
, priority_(priority < 1 ? 1 : (priority > 5 ? 5 : priority))
{}
Task(std::string title)
: Task(std::move(title), 3)
{
std::cout << "Task created with default priority.\n";
// Task created with default priority.
}
};
In real production you would not print from a constructor to the console (the user does not need to know you delegated internally), but for learning this is a good way to feel the execution order.
Delegation and invariants: the same rules without desynchronization
A constructor is the entry point into a valid state. But if you have 3 constructors and each normalizes data “in its own way”, you actually have not one class but three different versions of the same entity. Then you catch bugs that look like magic: “why does a task created via Task() behave differently than a task via Task(title)?”
Delegation cures this by forcing you to choose one place where you define the rules. Even if you are not a theory fan, this is practical: fewer places — fewer chances to forget.
Small “before / after” table:
| Approach | What happens | Risks |
|---|---|---|
| Several “independent” constructors | Each has its own checks and assignments | Rules desynchronize, copy-paste, hard to change |
| Main constructor + delegation | One constructor defines rules, others pick parameters | Shorter code, changes made in one place |
4. Limitations and anti-examples
Delegating constructors are powerful but not “magical”. They have several rules that are easier to accept as “laws of physics” than to try to circumvent.
A delegating constructor cannot both delegate to another constructor and separately initialize fields in the initialization list. In other words, you cannot write something like “I delegate and also tweak a field here”. The logic is: if you delegate, you hand over responsibility for initialization completely.
Another important point: delegation must have an endpoint. If you make a chain that goes in a circle, the compiler will not “guess what you meant” and will honestly say: “no”.
Bad example (don’t do this):
class Bad {
public:
Bad() : Bad(0) {} // delegating
Bad(int) : Bad() {} // delegating back (cycle)
};
This is conceptually similar to “I went to the store for bread, but returned home for the list, and left the list at the store”. An interesting story, but no bread.
5. Common mistakes
Mistake #1: copy-pasted checks remain, only now they are “nicely wrapped”.
Sometimes a student writes delegating constructors but still leaves checks and normalization in each of them “just in case”. As a result we get two problems at once: delegation exists, and desynchronization is still possible. Good style is: one main constructor contains the rules, others only prepare arguments and pass control along.
Mistake #2: trying to “slightly re-initialize fields” in the delegating constructor’s initialization list.
After delegation appears there is a temptation to write: Task() : Task("untitled", 3), priority_(5) {} — as if “I’ll just change the priority”. That’s not allowed: either you delegate or you initialize fields directly. If you need to change values — change delegation arguments or do it in the body (if it really makes sense and does not break invariants).
Mistake #3: delegation cycle.
The cycle can be obvious (as in the Bad example), or hidden through a chain of three constructors. The compiler will not accept it. A practical rule is simple: a delegation chain must have a “last station” — a constructor that actually initializes fields and does not delegate further.
Mistake #4: trying to perform important logic “before delegation”.
Sometimes you want “to compute something first, then delegate”. But delegation is written in the initialization list, and it runs before the constructor body. So if you need to prepare parameters, either do it via separate static helper functions (like normalize_title), or change the constructor design so the main constructor accepts already-prepared data.
Mistake #5: turning delegation into a “layered cake” with side effects.
If delegating constructors start to include prints, file writes, mutations of external objects and other effects, the call order becomes too important and the code fragile. For learning purposes std::cout is acceptable, but as a design habit it’s better to keep delegating constructors as thin and predictable as possible.
GO TO FULL VERSION