C++26: A User-Friednly assert() macro (sandordargo.com)

by jandeboevrie 82 comments 68 points
Read article View on HN

82 comments

[−] WalterBright 48d ago
D just makes assert() part of the language:

https://dlang.org/spec/expression.html#assert_expressions

The behavior of it can be set with a compiler switch to one of:

1. Immediately halting via execution of a special CPU instruction

2. Aborting the program

3. Calling the assert failure function in the corresponding C runtime library

4. Throwing the AssertError exception in the D runtime library

So there's no issue with parsing it. The compiler also understands the semantics of assert(), and so things like assert(0) can be recognized as being the end of the program.

[−] rurban 48d ago
So you are ignoring our well beloved NDEBUG? :)

Our idea of declare (optimize (speed 3) (safety 0))

[−] WalterBright 47d ago
D compilers have a "ramming speed" setting.
[−] panzi 46d ago
But does it have a ludicrous speed setting?
[−] MontagFTB 48d ago
Putting code with side effects into an assert is asking for trouble. Compile with NDEBUG set and the effects mysteriously disappear! Anything beyond an equality expression or straight boolean should be avoided.
[−] usrnm 48d ago
I once spent several days debugging that same mistake. Stuff worked perfectly in tests but broke misteriously in production builds. Couldn't stop laughing for a few minutes when I finally figured it out.
[−] bluGill 48d ago
Related our logging system has a debug which is not logged by default but can be turned on if a problem in an area is found (in addition to the normal error/info which is logged). I had the idea that if a test fails we should print all these debugs - easy enough to turn on but a number of tests failed because of side effects that didn't show up when off.

i'm trying to think of how/if we can run tests with all logging off to find the error and info logs with side effects.

[−] nyc_pizzadev 48d ago
This is just a symptom of a bad assert() implementation, which funny enough is the standard. If you properly (void) it out, side effects are maintained.

https://github.com/fiberfs/fiberfs/blob/7e79eaabbb180b0f1a79...

[−] omoikane 48d ago
assert() is meant to be compiled away if NDEBUG is defined, otherwise it shouldn't be called assert(). Given that assert() may be compiled away, it makes sense not to give it anything that has side effects.

Abseil has the convention where instead of assert(), users call "CHECK" for checks that are guaranteed to happen at run time, or "DCHECK" for checks that will be compiled away when NDEBUG is defined.

https://github.com/abseil/abseil-cpp/blob/0093ac6cac892086a6...

https://github.com/abseil/abseil-cpp/blob/0093ac6cac892086a6...

[−] nmilo 48d ago
If your assert compiles down to if (condition) {} in production then the compiler will optimize away the condition while keeping any side effects.
[−] IshKebab 48d ago
Yeah which may not be what you want. E.g. assert(expensive_to_compute() == 0).

The correct way to solve this is with debug asserts (as in Rust, or how the parent described).

[−] nyc_pizzadev 48d ago
Genuine question, does Rust know if expensive_to_compute() has side effects? There are no params, so could it be compiled out if the return value is ignored? Ex: expensive_to_compute() What about: (void) expensive_to_compute()?
[−] aw1621107 48d ago
No, in general Rust doesn't (and can't) know whether an arbitrary function has side effects. The compiler does arguably have a leg up since Rust code is typically all built from source, but there's still things like FFI that act as visibility barriers for the compiler.
[−] IshKebab 48d ago
No, Rust is the same as C++ in terms of tracking side effects. It doesn't matter that there are no parameters. It could manipulate globals or call other functions that have side effects (e.g. printing).
[−] functional_dev 48d ago
What about rust const fn()? I think it guarantees there are no side effects
[−] IshKebab 48d ago
I think you're right. Equivalent to C++'s constexpr.
[−] nmilo 48d ago
Compilers are very good these days. If it has no side effects it will likely be compiled out.
[−] samiv 48d ago
That's why you define your own assert macro and keep in on unconditionally. Your programs will be better for it.
[−] jandrewrogers 48d ago
An assertion can be arbitrarily expensive to evaluate. This may be worth the cost in a debug build but not in a release build. If all of assertions are cheap, they likely are not checking nearly as much as they could or should.
[−] samiv 48d ago
Possibly but I've never seen it in practice that some assert evaluation would be the first thing to optimize. Anyway should that happen then consider removing just that assert.

That being said being slow or fast is kinda moot point if the program is not correct. So my advisor to leave always all asserts in. Offensive programming.

[−] jmalicki 48d ago
Side effects are bad of course, but anything beyond a straight boolean or equality is bad?

assert(vector.size() < 3) is ridiculous to you?

[−] andrepd 48d ago
Rust has assert and debug_assert, which are self-explanatory. But it also has an assert_unchecked, which is what other languages incl C++ call an "assume" (meaning "this condition not holding is undefined behaviour"), with the added bonus that debug builds assert that the condition is true.
[−] tialaramex 48d ago
Notably, like most things with "unchecked" in their name core::hint::assert_unchecked is unsafe, however it's also constant, that is, we can do this at compile time, it's just promising that this condition will turn out to be true and so you should use it only as an optimisation.

Necessarily, in any language, you should not optimise until you have measured a performance problem. Do not write this because "I think it's faster". Either you measured, and you know it's crucial to your desired performance, or you didn't measure and you are wasting everybody's time. If you just scatter such hints in your code because "I think it's faster" and you're wrong about it being true the program has UB, if you're wrong about it being faster the program may be slower or just harder to maintain.

[−] saagarjha 48d ago
I actually feel like asserts ended up in the worst situation here. They let you do one line quick checks which get compiled out which makes them very tempting for those but also incredibly frustrating for more complex real checks you’d want to run in debug builds but not in release.
[−] maccard 48d ago
Indeed.

   bool is_even(int* valPtr) {
      assert(valPtr != nullptr);
      return *valPtr % 2;
    }
Does not do what you think it does with nullptr. A major game engine [0] has a toggle to enable asserts in shipping builds, mostly for this reason

[0] https://dev.epicgames.com/documentation/en-us/unreal-engine/...

[−] secondcoming 48d ago
Let's not vague post on HN. What's the problem with the above?
[−] saagarjha 48d ago
The problem is the code unconditionally dereferences the pointer, which would be UB if it was a null pointer. This means it is legal to optimize out any code paths that rely on this, even if they occur earlier in program order.
[−] pwdisswordfishy 48d ago
But if the assertion fails, the program is aborted before the pointer would have been dereferenced, making it not UB. This explanation is bogus.
[−] saagarjha 48d ago
Only if the assert is active. It basically means that the code is invalid when NDEBUG is set.
[−] quuxplusone 48d ago
When NDEBUG is set, there is no test, no assertion, at all. So yes, this code has UB if you set NDEBUG and then pass it a null pointer — but that's obvious. The code does exactly what it looks like it does; there's no tricks or time travel hiding here.
[−] teo_zero 48d ago

> it is legal to optimize out any code paths that rely on this, even if they occur earlier in program order.

I don't think this is true. The compiler cannot remove or reorder instructions that have a visible effect.

  if (p == 0)
    printf("Ready?\n");
  *p++;
The printf() can't be omitted.
[−] aw1621107 48d ago

> The compiler cannot remove or reorder instructions that have a visible effect.

You might be surprised! When it comes to UB compilers can and do reorder/eliminate instructions with side effects, resulting in "time travel" [0].

IIRC the upcoming version of the C standard bans this behavior, but the C++ standard still allows it (for now, at least).

[0]: https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=63...

[−] saagarjha 48d ago
No, this is explicitly legal. Most compilers will shy away from it these days since it made a lot of people upset, but it's definitely allowed.
[−] aw1621107 48d ago

> The problem is the code unconditionally dereferences the pointer, which would be UB if it was a null pointer.

Only when NDEBUG is defined, right?

[−] saagarjha 48d ago
No, the code that does this is always active
[−] gottheUIblues 48d ago
Right so strictly speaking C++ could do anything here when passed a null pointer, because even though assert terminates the program, the C++ compiler cannot see that, and there is then undefined behaviour in that case
[−] dccsillag 48d ago
I'm sorry, but what exactly is the problem with the code? I've been staring at it for quite a while now and still don't see what is counterintuitive about it.
[−] mhh__ 48d ago
This is a very "Dr Dr it hurts when I do this" "Don't do that" one it must be said.
[−] nealabq 48d ago
I don't mean to be that guy, but for "functional" programmers a print statement has "side effects".

But your meaning is clear. In an assert expression, don't call functions that might change the program/database state. Be as "const" as possible.

[−] nyc_pizzadev 48d ago
The nice thing about assert() is you can just define your own:

https://github.com/fiberfs/fiberfs/blob/7e79eaabbb180b0f1a79...

In this case, the ability to see the actual values that triggered the assert is way more helpful.

[−] omoikane 48d ago

> (assert) doesn't follow the usual SCREAMING_SNAKE_CASE convention we associate with macros

There are a few things like that, for example:

https://en.cppreference.com/w/c/numeric/math/isnan - isnan is an implementation defined macro.

https://en.cppreference.com/w/c/io/fgetc - getc may be implemented as a macro, but often it's a function.

[−] grokcodec 48d ago
Friedns shouldn't let Freidns post on HN without running spell check
[−] amelius 48d ago
Shouldn't the preprocessor be fixed, if it trips that easily on common C++ constructs?
[−] adzm 48d ago
One of my favorite things from ATL/WTL was the _ASSERT_E macro which additionally converts the source expression to text for a better message to be logged
[−] wpollock 48d ago

> assert(x > 0 && "x was not greater than zero");

Shouldn't that be "||" rather than "&&"? We want the message only if the boolean expression is false.

[−] throwpoaster 48d ago
assert(spellcheck(“Friednly”));
[−] semiinfinitely 48d ago
"C++47: Finally, a Standard Way to Split a String by Delimiter"