Constructors and Class Design in C++
The constructor, by contrast, is allowed to fail, so it must not be declared noexcept.
Every Function Makes a Promise
Every function makes a promise to the code that calls it. Part of that promise is whether the function can fail by throwing an exception. Some functions can always complete normally. Others depend on input, memory, files, or other conditions that may not hold. In this article you will see how C++ lets a programmer state the first kind of promise directly in the function's declaration, how constructors use the idea of an invariant to decide whether an object should exist at all, and how static_assert moves certain checks out of run time and into compilation.
These three ideas belong together. The first two are connected by the concept of an invariant, which is a condition that an object must satisfy. The third gives you a tool for checking conditions that can be known before the program ever runs.
Declaring a Function noexcept
A function declared noexcept is one whose author states that it may not throw. The keyword is part of the function's declaration, so anyone reading the signature can see the intended promise without reading the body.
The decision belongs to the function's author, who knows what the function does internally. The rule to remember is short: when you can truthfully say a function may not throw, mark it noexcept. The keyword is a claim, and it should be made only when the claim is true.
- The noexcept keyword sits after the parameter list, between the signature and the function body.
- The body only multiplies two integers, so nothing inside it can fail and the promise is truthful.
The square function has nothing in its body that can fail, so the noexcept declaration is a truthful statement about it. This is the easy case. The harder question is what to do with a function that cannot promise it will succeed, which the constructor section addresses.
How a Throw Travels Through Callers
A function does not work alone. It is called by other code, which is called by still other code, and the chain of active calls is the call stack. The noexcept promise matters because callers rely on it when they decide what they can assume about a call. The diagram below shows a chain of calls where the innermost function is the one that might throw. Read it from top to bottom and ask which callers sit above the point of failure, and where a noexcept promise would stop a caller from having to plan for an exception.
In this chain, handle_request carries a noexcept promise, which tells its callers that they need not prepare for an exception leaving it. The constructor, by contrast, is allowed to fail, so it must not be declared noexcept. The author of each function decides which promise is true for that function, and the signatures make those decisions visible.
Constructors That Establish an Invariant
An invariant is a condition that an object must satisfy. A constructor's role is to establish that invariant. The source's rule is that the constructor should let the object's invariant become true, and it should throw if it cannot establish one. The constructor should not create an object that quietly breaks its own condition. If the arguments given to the constructor cannot make the invariant true, the constructor refuses to finish and signals the problem by throwing.
The flowchart shows the two outcomes. On the success path, the constructor finishes and the caller receives an object whose invariant holds. On the failure path, the constructor throws and the object never comes into existence. The failure path is deliberate. It prevents any part of the program from holding an object that claims to be valid while violating its own rules.
Tracing an Object Through Its Lifetime
Consider the Percentage class from the source's example. Its job is to hold a value that is only meaningful between 0 and 100. That rule is the invariant. The code below is an illustrative version written to show how the constructor enforces the invariant and how the accessor, which cannot break the invariant, is declared noexcept.
- The constructor is the first place the invariant is enforced.
- The throw happens before the object is fully established, so a bad value never produces a usable Percentage.
- The accessor only reads the stored value, so it is declared noexcept.
Tracing Three Constructor Calls
Three Percentage objects are created with the values 40, 150, and 100. Which ones exist after their constructor calls, and which invariant decides each outcome?
- 1Create with 40
40 is between 0 and 100, so the invariant can be established. The constructor finishes and the object exists with value 40.
- 2Create with 150
150 is greater than 100, so the invariant cannot be established. The constructor throws, and no Percentage object is created.
- 3Create with 100
100 is within the allowed range, which includes the upper bound. The invariant holds and the object exists.
- 4Read the value
Calling
value()on the existing objects is safe because they satisfy the invariant, and the accessor is noexcept since it cannot break the invariant.
Only the objects created with 40 and 100 exist. The 150 attempt never yields an object, so no code can ever observe a Percentage that breaks the rule.
Designing Error Handling Around Invariants
The source advises that error-handling strategy should be designed around invariants. In practice, this means deciding where a program is allowed to be in a bad state and where it must not be. A class with a clear invariant gives its error handling a clear target: the object should never exist in a state that breaks its invariant. The constructor is the first place to enforce that rule, because it is where the object is created. Any function that changes the object must then keep the invariant true when it finishes.
The source does not list further error-handling patterns, so the guidance here is limited to that statement. Start by writing down the invariant for each class, then check that the constructor enforces it and that every mutating function preserves it. The Percentage example shows one application: the accessor is noexcept because reading the value cannot break the rule, while the constructor is where the rule is enforced.
Compile-Time Checks with static_assert
A static_assert declaration tests a condition while the program is being compiled, so a failing condition is reported before the program runs.
static_assert is useful when the condition depends only on things known at compile time, such as the size of a type or a constant defined in the code. Moving such a check to compile time means it costs nothing at run time, and it cannot be skipped because an input happened to avoid the faulty path. The source advises using static_assert for checks that can be made at compile time, since checking there is preferable when it is possible.
- kMaxSlots is a constant known while the code is compiled.
- The
static_assertchecks that constant. If someone changes kMaxSlots to 20, compilation fails with the message, before any run of the program.
Choosing Between Compile-Time and Run-Time Checks
The two kinds of check cover different parts of the same goal. A static_assert cannot examine a value that exists only when the program runs. The constructor check in the Percentage example depends on a value passed in during execution, so it must be a run-time throw. Compile-time checks handle what is fixed in the code, and run-time checks handle what arrives during execution.
Mistakes Beginners Make
Common mistake Marking a function noexcept when its body can fail
What it looks likeA function that parses user text and is declared noexcept, even though a bad string can make it fail.
Why it failsThe noexcept keyword is a truthful claim by the author. A false claim misleads every caller who relies on it.
Do this insteadMark a function noexcept only when you can truthfully say it may not throw, such as a simple accessor that only reads stored data.
Common mistake Letting a constructor build an object that breaks its own rule
What it looks likeA Percentage constructor that stores 150 without checking, so later code receives an object whose value is out of range.
Why it failsThe invariant is no longer true for that object, and every function that uses it must now defend against the bad state.
Do this insteadHave the constructor check the arguments and throw when the invariant cannot be established, so the bad object never exists.
Common mistake Using static_assert on a value that is only known at run time
What it looks likeTrying to write a static_assert that tests a number typed in by the user.
Why it failsstatic_assertruns while the program is compiled and cannot examine values that exist only during execution.Do this insteadUse a run-time check, such as a constructor that throws, for values that arrive while the program runs.
Common mistake Forgetting that a mutating function must also keep the invariant true when it finishes
Applying the Rules to a New Class
A class called Temperature stores a value in degrees Celsius that must be at least minus 273. Describe the invariant, say where it is enforced, state whether the accessor should be declared noexcept and why, and explain whether a static_assert could ever replace the check.
Hints
- Write the invariant as a sentence before you write any code.
- The constructor is the first place the invariant is enforced, since it creates the object.
- The accessor only reads stored data, so think about whether reading can break the invariant.
- The value passed to the constructor is supplied while the program runs, which affects which kind of check is possible.
Key Takeaways
- Declare a function noexcept only when you can truthfully say it may not throw, because the keyword makes that promise visible in the signature.
- A constructor establishes an object's invariant, and it should throw when it cannot establish one, so that no object breaking its own condition ever exists.
- Design error handling around invariants by enforcing them where objects are created and changed, and by keeping them true in every function that modifies the object.
- Use
static_assertfor checks that depend only on compile-time facts, and use run-time throws for values that arrive while the program executes.