C++ CPE study materials
C++ CPE study resources

Macros That Evaluate Twice: Radius Area and Retry Counter

C++ CPE · Put your knowledge to work in context.

All case studies

Macros That Evaluate Twice: Radius Area and Retry Counter

A legacy geometry module has been in production for nine years. Two macros are used across 40 source files:

#define AREA(r)   3.14159 * r * r
#define RETRY(c)  ((c)++ < 5 ? (c)++ : (c))

A new feature lets operators enter a radius as base + offset; the UI passes the expression r + 1 directly to AREA, and reports circle areas that are far too small whenever offset is nonzero, while values entered as a single number are correct. A separate subsystem calls RETRY(attempts) in a loop that is supposed to allow five failed logins before locking an account. QA observes that accounts lock after three failed attempts, and that attempts sometimes ends at 6. The build uses -O0 in debug and -O2 in release; the symptoms appear in both. Nobody wants to touch the 40 call sites in this release, and the module also compiles on a platform where int is 16 bits.

1. Expand `AREA(r + 1)` fully, apply C++ operator precedence, and compute the numeric result for `r = 10`. Compare it with the intended area and explain why whole-number inputs still work.

Read the worked answer

Macro expansion is textual, before any evaluation. AREA(r + 1) becomes 3.14159 * r + 1 * r + 1 because the body has no parentheses around r. Multiplication binds tighter than addition, so the expression is (3.14159 * r) + (1 * r) + 1. For r = 10, that is 31.4159 + 10 + 1 = 42.4159, while the intended 3.14159 * 11 * 11 is 380.133. The result is about nine times too small, and it is not a rounding effect. For a single number the substitution happens to be correct because the missing grouping does not change the parse. The fix is a function: constexpr double area(double r) { return 3.14159 * r * r; }. The argument r + 1 is evaluated once and passed as a value, so no parentheses are required at the call site, and the compiler can type-check the argument. Parenthesizing every parameter would also fix this case, but it must be done consistently and still leaves multiple evaluation for macros like RETRY.

2. Trace `RETRY(attempts)` starting at 0. Show why the account locks after three failures and why the counter reaches 6. Identify the second defect that occurs once the limit is exceeded.

Read the worked answer

RETRY(attempts) expands to ((attempts)++ < 5 ? (attempts)++ : (attempts)). The condition increments attempts whether or not the branch is taken. On the first call with attempts == 0, the condition compares 0 < 5, increments to 1, takes the true branch, and increments to 2. Each call adds 2. After three calls attempts is 6, the next condition is 6 < 5, and the account locks; QA sees three failures and a final value of 6. There is a second defect: even when attempts is already 5 or more, the condition still increments it, so the counter drifts upward once the limit is reached. A function taking a reference evaluates the argument once and makes the control flow explicit: bool retry(int& c) { if (c >= 5) return false; ++c; return true; }. If the intended semantics are five attempts total, the comparison belongs before the increment and the success path increments once. This is a case where the macro's textual reuse of c, not operator precedence, is the root cause.

3. Propose a migration that preserves the 40 existing call sites without changing their meaning, and explain why the function-based form is safer, including on the 16-bit `int` platform.

Read the worked answer

Functions are preferable for four reasons here: the argument is evaluated exactly once, the argument's type is checked, the body is visible to the debugger, and the call site does not depend on the macro's parenthesization. constexpr adds compile-time evaluation for constant arguments without changing runtime behavior. To avoid touching 40 call sites immediately, a single-evaluation compatibility wrapper is acceptable: #define RETRY(c) retry(c) and #define AREA(r) area(r); the argument appears once, so it is evaluated once, and the macro cannot double-increment or mis-group. That is a migration tool, not a destination: the uppercase macro names still pollute the global namespace and can collide. Rename to retry and area in the next release. The 16-bit int platform is not a problem for area because 3.14159 is a double; it is a reminder that macros capture types by text and can silently truncate or promote. Add unit tests for area(r + 1), retry at the boundary (4, 5, 6), and a call that passes a volatile counter to prove single evaluation.