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
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.
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.
#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.
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.