CodeRigor

FREE LESSON

Core Guidelines: checked expressions and scoped enums

Initialize before use, validate arithmetic and conversions before evaluation, and represent enum domains with explicit checked boundaries.

Small expressions can cross important boundaries: signed arithmetic, an array extent, a wire representation or an enum domain. The ES and Enum families ask us to make those boundaries visible. This lesson applies ES.20, ES.23, ES.25, ES.30–ES.31, ES.46, ES.100–ES.106 and Enum.1–Enum.8.

1. Initialize and constrain before narrowing

A cast says how to convert; it does not establish that a value fits. Brace initialization rejects many narrowing conversions at compile time. For a value read at run time, validate representability explicitly before converting. std::in_range supports integral range checks without guessing data-model widths or writing a signed/unsigned comparison incorrectly.

A conversion follows its proof
The range check uses the original value. Once a narrowing cast discards information it is too late to recover that evidence.
A conversion follows its proof
The range check uses the original value. Once a narrowing cast discards information it is too late to recover that evidence.
C++
int main() { int count{3.5}; }

Use a const local for a value that does not change, an initialized object instead of a later assignment, and a named helper when the boundary logic is important enough to reuse. Keep casts close to the check that justifies them.

2. Do arithmetic only when it is defined

Signed integer overflow is undefined behavior. Compute whether a + b fits before adding: for positive b compare a with max-b; for negative b compare a with min-b. The comparisons themselves must remain representable. Do not evaluate the overflowing sum and attempt to detect it afterwards.

Check before the dangerous operation
A rejected request produces an error value without evaluating an overflowing signed sum.
Check before the dangerous operation
A rejected request produces an error value without evaluating an overflowing signed sum.

Unsigned arithmetic wraps by definition, which can be useful for bit masks. It is not a universal replacement for quantities that must be nonnegative. An array index that arrives as a signed value still needs a negative check. The Guidelines discuss gsl::index; this exercise does not bundle GSL and uses ptrdiff_t plus explicit bounds checks at its interface.

C++
#include <cstddef>
#include <print>
#include <utility>
int main() {
    int index = -1; std::size_t size = 3;
    std::println("{} {}", index < size, std::cmp_less(index, size));
}

The raw comparison converts -1 to a large unsigned value. cmp_less compares the mathematical values safely, but a negative index is still not valid: check both nonnegativity and the upper bound before indexing.

3. Give related constants an enum domain

Scoped enums keep unrelated domains distinct and avoid implicit conversion to int. Prefer meaningful enumerator names rather than macros. Choose an explicit underlying type and numeric values when a wire format or bitmask requires them; avoid pinning them just because every enum could have a number.

An enum cast does not validate wire bits
Validate the full external representation before narrowing it to the underlying type. Typed operations then stay within the documented domain.
An enum cast does not validate wire bits
Validate the full external representation before narrowing it to the underlying type. Typed operations then stay within the documented domain.

A permission set can use typed operator| and a contains operation. Define their domain and the meaning of an empty request. Arbitrary cast enum values do not become meaningful permissions merely because they fit uint8_t. If you cast 256 first, you may discard the very bit that should reject the input.

4. Prefer ordinary language tools to expression macros

Use a constexpr function or a small named function for calculations, and a typed constant or enum for named values. A macro that repeats an argument can repeat its side effects. Parentheses help precedence but do not establish single evaluation or a type contract. Headers still need include guards, and platform/feature-test macros have legitimate jobs; keep necessary macro names distinct and ALL_CAPS. Do not blindly delete build configuration macros.

Apply it

Implement checked addition, conversion, indexing and permission parsing. Test the exact minimum/maximum representable values, zero-size sequences, negative indices, and unknown high bits. No failing branch should first perform the undefined operation it claims to prevent. Existing sanitizer exercises teach how violations appear; these interfaces teach how to reject invalid input before creating that violation.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Watch for the design rationale; use the authored lesson and current guideline links for the precise exercise contract.