C++ Part I · Module 12 · Constructors and Destructors
Writing Constructors
A constructor has one job that matters, which is to leave the object in a state the type is willing to vouch for, and the previous chapter established why it is the only place that job can be done: by the time the body runs every member exists, and when the body finishes the object's lifetime has begun and the rest of the program may use it. What makes this a design chapter rather than a syntax chapter is that most types have more than one legitimate starting point. A register field might arrive as an offset and a width, or as a first and last bit the way a datasheet prints it, or as a single bit number. All three describe the same thing, all three should end up with the same checked invariant, and the obvious move is to write a constructor for each. That turns out not to be available, for a reason the compiler states bluntly and which is measured here: the first two have the same signature, so they are not two constructors at all. Discovering that changes the shape of the answer — one constructor does the checking and the alternative ways in become named functions that route through it — and it also exposes a distinction worth keeping, between a condition that is part of the type's invariant and a condition that merely belongs to one particular way of describing it.
Foundation17 min readC++Part IFundamentalsConstructors
Part I · Module 12 · Chapter 12.2 · Writing Constructors
1. The One Job
class Field {
public:
Field(int offset, int width) : offset_(offset), width_(width)
{
if (offset_ < 0 || width_ <= 0 || offset_ + width_ > 32)
throw std::invalid_argument("invalid field");
}
private:
int offset_; int width_;
};A constructor is a member function with the class's name and no return type. That is the entire syntax. Its job:
Leave the object in a state the type is willing to vouch for — or do not let the object exist.
Chapter 11.5 §3 called that establishing the invariant, and Chapter 12.1 §5 explained why a constructor is the only place it can happen: the body is the last step of construction, and the lifetime begins when it completes.
2. The Form
Field(int offset, int width) // name, parameters, no return type
: offset_(offset), width_(width) // member initializer list — 12.3
{
/* body: whatever is left, usually the checking */
}Three parts, and the middle one has its own chapter.
The signature — the class name and a parameter list.
The member initializer list, after a colon, before the brace. This is where members are initialized. Chapter 12.1 §7 explained why it exists at all and Chapter 12.3 measures the three cases where nothing else will do; every listing here uses it, and that chapter owns the argument.
The body, which runs after every member exists.
No return type, and no return value — a constructor does not return anything, it makes something.
3. Where the Check Goes
In the body, after the initializer list has set the members. That order matters: you want to validate the values as they will actually be stored.
Field(int offset, int width) : offset_(offset), width_(width)
{
if (offset_ < 0 || width_ <= 0 || offset_ + width_ > 32)
throw std::invalid_argument("invalid field");
}Checking offset_ rather than offset is deliberate — it validates the object, not the arguments, which matters once a constructor does any conversion or clamping on the way in.
Chapter 11.5 §6's three ways to refuse all apply: throw when a caller may reasonably pass bad data, return a bool from an operation that can routinely fail (not available to a constructor — §8), or assert for a programmer error. What is never acceptable is finishing with the invariant violated.
4. Several Ways In — the Obvious Attempt
A field is described two ways in practice. Datasheets usually print the bit range; code usually wants the offset and width. So:
class Field {
public:
Field(int offset, int width); // offset + width
Field(int firstBit, int lastBit); // first + last
};error: constructor cannot be redeclared
note: previous declaration is hereThey are not two constructors. Both take (int, int), and a function's parameter names and intended meanings are not part of its signature — only the types and their order are. The compiler sees the same constructor declared twice.
This is worth sitting with, because it is a real constraint on the frozen advice to "overload for the ways a caller legitimately has the data": you can only overload when the data shapes differ. For engineering types, where most quantities are integers, they very often do not.
5. What Does Distinguish Overloads
Measured, with five constructors and 0 diagnostics:
class Reg {
public:
Reg(); // arity 0
explicit Reg(int addr); // one int
Reg(int addr, int width); // two ints
explicit Reg(const std::string& n); // a string
explicit Reg(double d); // a double
};Reg()
Reg(int 64)
Reg(int 64, int 32)
Reg(string "CTRL")
Reg(double 1.5)So overloads are distinguished by how many parameters and what types — nothing else. (The explicit on the single-argument ones is Chapter 12.5's subject and is there because omitting it has a measured consequence.)
And overloading on two numeric types has its own trap:
class R { public: explicit R(int); explicit R(double); };
R c(1u); // unsignederror: call to constructor of 'R' is ambiguousunsigned converts equally well to int and to double, so neither overload wins. Chapter 6.7 covers overload resolution generally; the practical lesson is to avoid overloading on numeric types that convert into each other.
6. The Answer: One Checking Constructor, Named Ways In
Since §4's two descriptions cannot both be constructors, give them names:
class Field {
public:
// THE one that checks
Field(int offset, int width) : offset_(offset), width_(width) { check("offset+width"); }
// how a datasheet prints it
static Field fromBits(int firstBit, int lastBit)
{
if (lastBit < firstBit) throw std::invalid_argument("lastBit < firstBit");
return Field(firstBit, lastBit - firstBit + 1);
}
static Field singleBit(int bit) { return Field(bit, 1); }
int offset() const { return offset_; }
int width() const { return width_; }
int last() const { return offset_ + width_ - 1; }
private:
void check(const char* via) const;
int offset_; int width_;
};Measured:
three ways in, one invariant:
checked via offset+width -> offset 4 width 3
checked via offset+width -> offset 4 width 3
checked via offset+width -> offset 7 width 1
a: 4..6 b: 4..6 c: 7..7
a and b describe the same field? yesField(4, 3) and Field::fromBits(4, 6) produced identical objects, and every route printed checked via offset+width — the checking happened once, in one place, no matter how the caller arrived.
These are usually called named constructors (or static factory functions). Four things they buy:
They can share a signature. fromBits(4, 6) and Field(4, 3) both take two ints and coexist.
The name says what the arguments mean. fromBits(4, 6) cannot be misread; Field(4, 6) can.
The invariant stays in one place. Chapter 12.6 is the mechanism — these factories delegate to the checking constructor, and that chapter's whole purpose is routing several constructors through one.
And they can fail before constructing anything, which is §7.
(A third natural way in — copy an existing field and change one value — is a copy constructor, which is Module 13's subject and deliberately not here.)
7. Two Kinds of Condition
Every route is checked, and the measured refusals show something worth noticing:
and every route is checked:
Field(30, 8) REJECTED: invalid field
Field::fromBits(28, 35) REJECTED: invalid field
Field::fromBits(6, 4) REJECTED: lastBit < firstBit
Field::singleBit(40) REJECTED: invalid fieldThree of those say invalid field and one says lastBit < firstBit. That is not sloppiness — they are different kinds of condition.
The invariant belongs to the type: offset >= 0, width > 0, offset + width <= 32. Every object must satisfy it however it was made, so it is checked in the constructor.
A precondition on one way in belongs to that route: lastBit >= firstBit is meaningless for the offset-and-width form, which has no lastBit. So it is checked in fromBits, before delegating.
Chapter 11.9 §4 drew the same line from the other direction, deciding that "a value cannot be wider than the field" was a precondition of the write operation rather than part of the field's invariant. The general rule:
If the condition can be stated without reference to a particular way of constructing the object, it is the invariant. Otherwise it is a precondition of that route.
Getting this wrong in the harmless direction — putting a route's precondition in the invariant — forces the invariant to mention things most callers have never heard of. Getting it wrong in the harmful direction leaves a route unchecked.
8. A Constructor Cannot Report Failure by Returning
A constructor has no return value, so the usual "return false" is unavailable. The options are exactly:
Throw, which is what these examples do, and which is right when a caller may reasonably supply bad data — a parsed file, a configuration value, user input. Chapter 22.2 is throw, try and catch properly.
Assert, when invalid input means a programmer error rather than bad data. Chapter 25.1's undefined-behaviour material is adjacent; the distinction is whether the condition could occur in a correct program.
Or make the failure impossible by narrowing the parameter types, which is always the best option when available. Chapter 9.9 measured enum class making an invalid access mode unrepresentable, so no check was needed at all, and Chapter 11.9 §4 used exactly that.
And one option that is not on the list: finishing the constructor with the object invalid and an ok_ flag set to false. Chapter 11.5's callout rejected it — every subsequent operation must then check the flag, which restores the invalid state the type existed to forbid.
Why throwing from a constructor is safe, which is the fact that makes this workable. Measured:
constructor body throws, after both members built:
+ member a
+ member b
body begins; about to throw
- member b
- member a
caught: ctor failedThe already-constructed members were destroyed in reverse order, and ~Throws — the object's own destructor — never ran, because the object never existed. Throwing mid-way through the members is handled too:
throw while building the SECOND of three members:
+ member a
- member a
caught: member ctor failedOnly a had been built, so only a was destroyed; c was never started. Chapter 12.7 §8 owns this; the consequence for now is that a constructor is a safe place to acquire a resource, which is Chapter 12.9's foundation.
(A failure that is expected rather than exceptional is better modelled by a factory returning std::optional, named in Chapter 9.11 §7 and belonging to Part II — a named constructor can do that, and a real constructor cannot.)
9. What to Actually Do
Give the type one constructor that establishes the invariant, and check in the body after the initializer list has set the members.
Validate the members, not the parameters — it is the object you are vouching for.
Use named factories for alternative ways in, especially when the signature would collide.
Route every factory through the checking constructor (Chapter 12.6), so the invariant is written once.
Keep a route's precondition in that route, and the type's invariant in the constructor.
Overload only on arity and genuinely distinct types, and never on numeric types that convert into each other.
Throw, assert, or narrow the types — never finish invalid, never use an ok_ flag.
Ask of every constructor: does a caller have the data in exactly this shape? If not, delete it.
And do not add a default constructor unless there is a valid default state (Chapter 12.4).
10. Mistakes Worth Making Once
Writing two constructors with the same parameter types. Measured: error: constructor cannot be redeclared — names and meanings are not part of the signature.
Overloading on int and double. Measured: R c(1u) is ambiguous.
Copying the invariant check into every constructor. Four places to drift; route them through one.
Putting a route's precondition in the invariant. lastBit >= firstBit is meaningless for the offset/width form.
Leaving a factory unchecked because "the constructor checks" — it does, but only what it can see.
Validating the parameters instead of the members, which diverges as soon as the constructor converts anything.
Finishing with an ok_ flag. Chapter 11.5's rejected design.
Adding a default constructor for convenience. Chapter 11.1 §6 measured the type correctly refusing one.
Assuming a throwing constructor leaks its members. Measured: destroyed in reverse, and the object's destructor never runs.
11. Interview Relevance
"What is a constructor's job?" To leave the object in a valid state or prevent it existing. The strong answer connects it to Chapter 12.1's ordering: it is the last step of construction, so the lifetime begins when it completes.
"Can you have two constructors taking two ints with different meanings?" No — error: constructor cannot be redeclared. This is a good discriminator, and the follow-up ("so what do you do?") wants named factories.
"How does a constructor report failure?" Throw, assert, or narrow the types so failure is unrepresentable. Noting that it cannot return a value, and that an ok flag reintroduces the invalid state, is the complete answer.
"What happens if a constructor throws halfway through?" Members already constructed are destroyed in reverse order; the object's destructor does not run because the object never existed. This is the fact that makes RAII safe and it is often not known.
"Where do you put the invariant check?" In the one constructor that every other route delegates to — so it is written once.
12. What You Should Be Able to Say Now
- A constructor's job is to leave the object valid or prevent it existing — and it is the last step of construction (Chapter 12.1).
- The form is name, parameters, no return type, with an initializer list before the body.
- Check in the body, against the members, after the initializer list has set them.
- Measured: two constructors taking
(int, int)iserror: constructor cannot be redeclared— parameter names and meanings are not part of the signature. - So overloading only works when the data shapes differ, which for engineering types often they do not.
- Measured: overloads are distinguished by arity and type — five constructors selected correctly with 0 diagnostics.
- Measured:
intanddoubleoverloads makeR c(1u)ambiguous. - The answer is one checking constructor plus named factories — measured, three routes all printing
checked via offset+width, andField(4,3)identical toField::fromBits(4,6). - Measured: all four invalid routes refused.
- Distinguish the invariant from a route's precondition —
lastBit < firstBitwas caught by the factory, not the constructor. - A constructor cannot return a failure — throw, assert, or narrow; never an
ok_flag. - Measured: a throwing constructor destroys built members in reverse and the object's destructor never runs.
- Ask of each constructor whether a caller has the data in exactly that shape.
13. Exercises
Exercise 1 — write one. Give a Counter class with a maximum, a constructor taking the maximum, and the invariant max > 0. Say where the check goes.
Exercise 2 — predict. Which of these can coexist in one class: C(int), C(int, int), C(double), C(int, int) again, C(const std::string&), C()?
Exercise 3 — the collision. A Range can be given as (lo, hi) or as (start, count). Say what happens if you write both as constructors, and give your design.
Exercise 4 — invariant or precondition? For Field, classify each: (a) width > 0 (b) lastBit >= firstBit (c) offset + width <= 32 (d) the string "7:4" parses (e) offset >= 0.
Exercise 5 — the ambiguity. Explain why R c(1u) is ambiguous with int and double overloads, and give two fixes.
Exercise 6 — how many? For a Transaction with an address, data and a direction, propose the constructors and factories you would provide, applying the callout's test to each.
Exercise 7 — the throw. Trace the output for a class with three printing members whose constructor body throws, and say whether its destructor runs.
Exercise 8 — refuse it. A constructor takes an access mode as an int and validates it. Give the better design and say which check disappears.
14. Summary
- A constructor leaves the object valid or prevents it existing — the last step of construction.
- Name, parameters, no return type, with the initializer list before the body; check the members in the body.
- Measured:
(int,int)twice iserror: constructor cannot be redeclared— names and meanings are not signature. - Overloads are distinguished by arity and type only, and
int/doubleoverloads madeR c(1u)ambiguous. - So: one checking constructor plus named factories — measured, three routes, one check, identical objects from different inputs, four invalid routes refused.
- The invariant belongs to the type; a route's precondition belongs to the route.
- A constructor cannot return failure — throw, assert, or narrow the types; never an
ok_flag. - Measured: a throwing constructor destroys built members in reverse and its own destructor never runs — which is what makes resource acquisition safe.
- Ask whether a caller has the data in exactly that shape, and do not invent a default state.
Every listing in this chapter has used the colon-and-comma clause between the signature and the body, and it has now been deferred by four chapters: Chapter 11.9 named it and waved past, Chapter 12.1 explained why it must exist, and this chapter used it in every example. The reason it keeps being postponed is that its real justification is not the one usually given. It is normally presented as a performance point — initializing is cheaper than assigning — and that is true and measurable, and it is the smaller half. The larger half is that for three specific kinds of member there is no such thing as assigning afterwards, because there is no valid earlier state to assign over, and the compiler says so in three parallel messages. The next chapter measures the cost in one case and the impossibility in the other three.
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.
