Date: Fri, 17 Oct 2025 06:28:27 -0700
Harald asked:
> On the internet I saw someone saying
>
> void fun(Foo* ptr) pre (ptr!=nullpter), pre(ptr->hasData()) { ... }
>
> might be a problem (for the second pre) and should be written like this
>
> void fun(Foo* ptr) pre (ptr!=nullpter && ptr->hasData()){ ... }
>
> is that true?
With ignore, enforce, or quick_enforce there’s no issue. The second check will not be evaluated, no UB.
With observe semantics (only), if the pointer is null then:
* the first check fails, and gives a contract violation so you know it was violated
* the second check then gives UB (no time travel, does not affect the first check that already happened)
IMO the issue isn’t about contracts, it’s about short-circuit evaluation. This kind of issue can come up generally in the language if you split the (p) and (p->foo()) conditions, so we already teach that to get short-circuit evaluation you need to write the && using (p && p->foo()).
This also comes up (again, generally throughout the language) with other examples like
(is_sorted(v) && binary_search(v, x))
because implementations of binary_search might try to perform unsafe out-of-bounds access if the range is not already sorted.
In case it's helpful, here's the current slide I teach about that example (updated and extended since CppCon):
Herb
> On the internet I saw someone saying
>
> void fun(Foo* ptr) pre (ptr!=nullpter), pre(ptr->hasData()) { ... }
>
> might be a problem (for the second pre) and should be written like this
>
> void fun(Foo* ptr) pre (ptr!=nullpter && ptr->hasData()){ ... }
>
> is that true?
With ignore, enforce, or quick_enforce there’s no issue. The second check will not be evaluated, no UB.
With observe semantics (only), if the pointer is null then:
* the first check fails, and gives a contract violation so you know it was violated
* the second check then gives UB (no time travel, does not affect the first check that already happened)
IMO the issue isn’t about contracts, it’s about short-circuit evaluation. This kind of issue can come up generally in the language if you split the (p) and (p->foo()) conditions, so we already teach that to get short-circuit evaluation you need to write the && using (p && p->foo()).
This also comes up (again, generally throughout the language) with other examples like
(is_sorted(v) && binary_search(v, x))
because implementations of binary_search might try to perform unsafe out-of-bounds access if the range is not already sorted.
In case it's helpful, here's the current slide I teach about that example (updated and extended since CppCon):
Herb
Received on 2025-10-17 13:28:30
