[Development] Disavowing the Lakos Rule for Q_ASSERT
Ville Voutilainen
ville.voutilainen at gmail.com
Wed Aug 28 14:58:16 CEST 2024
On Wed, 28 Aug 2024 at 13:26, Volker Hilsheimer via Development
<development at qt-project.org> wrote:
> Since Lakos is referenced in this thread, two quotes from the paper [1] that resulted in P3155R0 [2], aka “The Lakos Rule”:
>
> On page 5 of [1]:
>
> > What Are We Proposing
> > In order to support effective testing, compilers should offer two ‘modes’ for handling noexcept violations.
> >
> > i/ a “production” mode, the default, which guarantees to terminate the process rather than allow an exception to propagate past a noexcept exception specification.
> >
> > ii/ a “testing” mode, which performs regular stack unwinding if an exception propagates beyond a noexcept exception specification. This would imply disabling any optimizations based on static analysis of exception specifications, while ensuring that the noexcept operator continues to see the same result.
>
> On page 8 of [1]:
>
> > Preferred additional recommendation
> > If the core language can be amended to support a testing mode, we recommend the following guideline:
> > Each operation whose behavior is such that it clearly cannot throw, when called with arguments satisfying function preconditions (and its own object state invariants), should be marked as unconditionally noexcept.
>
> The core language, as of today, has not been amended to support a “testing mode” (where, as per proposal, an exception propagating past a noexcept function would not terminate).
And won't be. So, the second additional recommendation there is no
longer pursued by anyone, whereas the alternative library
recommendation,
i.e.
"Remove noexcept specifications from each library function having a narrow
contract, typically (but not always) indicated by the presence of a
Requirements:
clause."
is. There has been movement away from it, and now there's pushback (ha
ha) towards where we were before, i.e. towards the so-called Lakos
Rule,
so that functions with preconditions aren't made noexcept.
However, Q_ASSERT as a macro that expands to nothing in a “production”
mode build, seems to me to be very similar in spirit: we don’t assert
in a release build, so we can guarantee that we don’t throw an
exception in code like the referenced
>
> static Qt::strong_ordering compareThreeWay_helper(const Iterator &lhs,
> const Iterator &rhs) noexcept
> {
> Q_ASSERT(lhs.item.d == rhs.item.d);
> return Qt::compareThreeWay(lhs.item.i, rhs.item.i);
> }
>
> This function, when called with arguments satisfying function preconditions, cannot throw. In the “test” mode, aka “debug build”, it does whatever we want Q_ASSERT to do.
You would need to make that noexcept then conditional on the build
mode. I don't know exactly why that was problematic for libc++, but
they tried
it and ended up throwing.. ..I mean ruling that approach out. It was
more trouble than it was worth, because the noexceptness changes in
release
and test modes ended up being problematic.
I'm not sure why. Functions with narrow contracts aren't what you
typically use in move operations, and move operations are where you
typically
care about noexcept.
> Making it possible for us to test the effectiveness of asserts, esp when they are used to implement a “contract”, is a good goal. Throwing an exception would be one way to do that, but not the only one. Fork’ing, or using an assert handler, or defining Q_ASSERT to spit out a qCritical that makes the test fail (via QTest::failOnWarning), are all possible ways to do that today already.
Right. Doing death tests with forked processes is one option, but it
doesn't scale well on all platforms. See
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2831r0.pdf
and in particular section 2, Negative Testing.
> One can argue that comparing iterators from two different lists resulting in undefined behavior is not an unexpected issue; the behavior of comparing two iterators over different ranges is undefined [3] (unless the respective comparison operator has an explicit noexcept specification). Does the same logic apply for calling std::vector::operator[] with an index out of bounds? Is it material to the principle that the index can come from somewhere else, and that the vector is probably long-lived and might have changed after the boundaries were asserted? It is well-defined that calling std::optional::value on a nullopt throws an exception, while it is not defined what operator[] does (but it will be, once a C++26 contract is added to it).
>
> So, somewhat marginal technical benefits aside, what “noexcept” in a library API communicates to the caller of the API is whether or not they should consider exception handling as a tool to make their code more resilient. We do need to make sure that users can handle problems gracefully so that their program becomes resilient against unexpected issues. We already make it close to impossible to use C++ exception handling to that end (ref recent thread [4]).
That sounds fine.. ..sort of. But it also sounds like an overuse and
misuse of noexcept, that isn't why it was added to the language.
And there always has been functions that aren't noexcept, but are
otherwise documented not to throw exceptions.
More information about the Development
mailing list