What happened
#663's fix (PR #676) bounds bankgui::fmt::parseMinor's input so an out-of-range amount is rejected instead of being converted out of range. To do that it splits the expression:
- return static_cast<std::int64_t>(major * scale + 0.5);
+ const double minor = (major * scale) + 0.5;
+ if (!(minor < kMinorUnitsBound)) { return std::nullopt; }
+ return static_cast<std::int64_t>(minor);
The rounding behaviour is unchanged. What did change is that the line no longer matches bugprone-incorrect-roundings, which was reporting on it before:
$ clang-tidy -p build/clang-debug --extra-arg=-std=c++23 \
--extra-arg=-Wno-missing-include-dirs --quiet \
examples/bank/gui/controllers/Format.hpp # pre-fix content, on 563502ab
/.../examples/bank/gui/controllers/Format.hpp:76:38: error: casting (double + 0.5) to integer leads to incorrect rounding; consider using lround (#include <cmath>) instead [bugprone-incorrect-roundings,-warnings-as-errors]
So a check that was pointing at this arithmetic has stopped pointing at it, without the question it was raising being answered. #663's filer had already flagged + 0.5 and labelled the concern weak; the triage said to decide it separately rather than fold an unmeasured rounding change into a UB fix, which is what PR #676 did. This is that separate ticket.
The substance of the check's complaint
static_cast<int64_t>(x + 0.5) is not round-half-away-from-zero. x + 0.5 is itself a rounded double operation, so a value just below a half can be rounded up to exactly the next integer by the addition and then truncate to the wrong side. The textbook witness is 0.49999999999999994, the largest double below 0.5: 0.49999999999999994 + 0.5 == 1.0 in IEEE-754 double, so the expression yields 1 where correct rounding yields 0. std::llround does not have that behaviour.
Whether any such value is reachable here is the open question, and it is a narrower one than the general case, because x is always major * scale for scale a power of ten and major a value QString::toDouble produced from user text. I have not searched for a reachable witness.
Verification status
What would change the verdict
- Close as
invalid if someone shows the input domain cannot produce a witness — e.g. because QString::toDouble output times a power of ten can never land on a double of that shape. That argument needs writing down either way, since it is the thing the current code silently depends on.
- Close as
valid and fix by replacing the expression with std::llround(major * scale) inside the existing bound, plus a test carrying the witness value.
- Re-open if
bugprone-incorrect-roundings is later re-triggered by an unrelated edit to this function, which would mean the split was load-bearing for the suppression rather than incidental.
Found while working #663 (PR #676). Filed, not folded.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
What happened
#663's fix (PR #676) bounds
bankgui::fmt::parseMinor's input so an out-of-range amount is rejected instead of being converted out of range. To do that it splits the expression:The rounding behaviour is unchanged. What did change is that the line no longer matches
bugprone-incorrect-roundings, which was reporting on it before:So a check that was pointing at this arithmetic has stopped pointing at it, without the question it was raising being answered. #663's filer had already flagged
+ 0.5and labelled the concern weak; the triage said to decide it separately rather than fold an unmeasured rounding change into a UB fix, which is what PR #676 did. This is that separate ticket.The substance of the check's complaint
static_cast<int64_t>(x + 0.5)is not round-half-away-from-zero.x + 0.5is itself a rounded double operation, so a value just below a half can be rounded up to exactly the next integer by the addition and then truncate to the wrong side. The textbook witness is0.49999999999999994, the largest double below 0.5:0.49999999999999994 + 0.5 == 1.0in IEEE-754 double, so the expression yields 1 where correct rounding yields 0.std::llrounddoes not have that behaviour.Whether any such value is reachable here is the open question, and it is a narrower one than the general case, because
xis alwaysmajor * scaleforscalea power of ten andmajora valueQString::toDoubleproduced from user text. I have not searched for a reachable witness.Verification status
563502abwith clang 22.1.8 and theclang-tidyjob's own configure. Also reproduced that it no longer fires after PR bank/gui: bound parseMinor's input so an out-of-range amount is rejected rather than undefined (fixes #663) #676's split.parseMinor("0.005") == 1before and after, and a sweep of 24576 doubles across the accept/reject boundary fordecimals0/1/2 gives identical accepted values.x + 0.5rounds to the wrong side. I did not search for one. It may well be unreachable, in which case the honest resolution is a comment saying so rather than a code change.std::llroundwould be a drop-in. It rounds half away from zero, which for non-negative values matches the intent, but it is also UB on an out-of-range argument — so it would have to sit inside the bound PR bank/gui: bound parseMinor's input so an out-of-range amount is rejected rather than undefined (fixes #663) #676 added, not replace it.What would change the verdict
invalidif someone shows the input domain cannot produce a witness — e.g. becauseQString::toDoubleoutput times a power of ten can never land on a double of that shape. That argument needs writing down either way, since it is the thing the current code silently depends on.validand fix by replacing the expression withstd::llround(major * scale)inside the existing bound, plus a test carrying the witness value.bugprone-incorrect-roundingsis later re-triggered by an unrelated edit to this function, which would mean the split was load-bearing for the suppression rather than incidental.Found while working #663 (PR #676). Filed, not folded.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW