NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
C++26: A User-Friednly assert() macro (sandordargo.com)
WalterBright 23 hours 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 12 hours ago [-]
So you are ignoring our well beloved NDEBUG? :)

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

MontagFTB 1 days 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.
bluGill 22 hours 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.

usrnm 1 days 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.
nyc_pizzadev 1 days 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 1 days 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 24 hours 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 21 hours 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 20 hours 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 12 hours 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 12 hours 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 9 hours ago [-]
What about rust const fn()? I think it guarantees there are no side effects
IshKebab 8 hours ago [-]
I think you're right. Equivalent to C++'s constexpr.
nmilo 15 hours ago [-]
Compilers are very good these days. If it has no side effects it will likely be compiled out.
samiv 1 days ago [-]
That's why you define your own assert macro and keep in on unconditionally. Your programs will be better for it.
jandrewrogers 1 days 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 1 days 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.

saagarjha 20 hours 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.
jmalicki 1 days 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?

maccard 1 days 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 23 hours ago [-]
Let's not vague post on HN. What's the problem with the above?
saagarjha 20 hours 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 14 hours 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 12 hours ago [-]
Only if the assert is active. It basically means that the code is invalid when NDEBUG is set.
quuxplusone 6 hours 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 13 hours 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 12 hours 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 12 hours 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 20 hours 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 19 hours ago [-]
No, the code that does this is always active
aw1621107 12 hours ago [-]
Shouldn't control flow diverge if the assert is triggered when NDEBUG is not defined? Pretty sure assert is defined to call abort when triggered and that is tagged [[noreturn]].
saagarjha 12 hours ago [-]
Sorry, yes, I misread you
gottheUIblues 19 hours 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
aw1621107 12 hours ago [-]
> because even though assert terminates the program, the C++ compiler cannot see that

I think it should be able to. I'm pretty sure assert is defined to call abort when triggered and abort is tagged with [[noreturn]], so the compiler knows control flow isn't coming back.

dccsillag 1 days 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.
dataflow 22 hours ago [-]
Depends on where you're coming from, but some people would expect it to enforce that the pointer is non-null, then proceed. Which would actually give you a guaranteed crash in case it is null. But that's not what it does in C++, and I could see it not being entirely obvious.
IshKebab 21 hours ago [-]
Assert doesn't work like that in any language.
comex 21 hours ago [-]
It does in Rust: assert is always enabled, whereas the debug-only version is called debug_assert.

But yes, “assert” in most languages is debug-only.

IshKebab 12 hours ago [-]
He said

> some people would expect it to enforce that the pointer is non-null, then proceed

No language magically makes the pointer non-null and then continues. I don't even know what that would mean.

18 hours ago [-]
IshKebab 1 days ago [-]
There's nothing wrong with it. It does exactly what you think it does when passed null.
jmalicki 16 hours ago [-]
A lot of compilers will optimize out a NULL pointer check because dereferencing a NULL pointer is UB.

Because assert will not run the following code in the case of a NULL pointer, AFAIK this exact code is still defined behavior, but if for some reason some code dereferenced the NULL pointer before, it would be optimized out - there are some corner cases that aren't obvious on the surface.

This kind of thing was always theoretically allowed, but really started to become insidious within the past 5-10 years. It's probably one of the more surprising UB things that bites people in the field.

GCC has a flag "-fno-delete-null-pointer-checks" to specifically turn off this behavior.

https://qinsb.blogspot.com/2018/03/ub-will-delete-your-null-...

This is an actual Linux kernel exploit caused by this behavior where the compiler optimized out code that checked for a NULL pointer and returned an error.

https://lwn.net/Articles/342330/

IshKebab 12 hours ago [-]
Sure, but none of that is relevant to just the code snippet that was posted. The compiler can exploit UB in other code to do weird things, but that's just C being C. There's nothing unexpected in the snippet posted.

The issue is cause by C declaring that dereferencing a null pointer is UB. It's not really an issue with assertions.

You can get the same optimisation-removes-code for any UB.

maccard 10 hours ago [-]
> There's nothing unexpected in the snippet posted.

> The issue is cause by C declaring that dereferencing a null pointer is UB. It's not really an issue with assertions. > You can get the same optimisation-removes-code for any UB.

I disagree - It’s a 4 line toy example but in a 30-40 line function these things are not always clear. The actual problem is if you compile with NDEBUG=1, the nullptr check is removed and the optimiser can (and will, currently) do unexpected things.

The printf sample above is a good example of the side effects.

IshKebab 8 hours ago [-]
> The actual problem is if you compile with NDEBUG=1

That is entirely expected by any C programmer. Sure they named things wrong - it should have been something like `assert` (always enabled) and `debug_assert` (controlled by NDEBUG), as Rust did. And I have actually done that in my C++ code before.

But I don't think the mere fact that assertions can be disabled was the issue that was being alluded to.

maccard 7 hours ago [-]
I wrote the comment, assertions being disabled was exactly what was being alluded to.

> that is entirely expected by any C programmer

That’s great. Every C programmer also knows to avoid all the footguns and nasties - yet we still have issues like this come up all the time. I’ve worked as a C++ programmer for 12 years and I’d say it’s probably 50/50 in practice how many people would spot that in a code review.

IshKebab 4 hours ago [-]
It's definitely a footgun, but the compiler isn't doing weird stuff because the assertions can be disabled. It's doing weird stuff because there's UB all over the place and it expects programmers to magically not make any mistakes. Completely orthogonal to this particular (fairly minor IMO) footgun.

> I’ve worked as a C++ programmer for 12 years and I’d say it’s probably 50/50 in practice how many people would spot that in a code review.

Spot what? There's absolutely nothing wrong with the code you posted.

jmalicki 1 hours ago [-]
That assert could completely fail to fire if inlined into another function that did a dereference first.
mhh__ 1 days ago [-]
This is a very "Dr Dr it hurts when I do this" "Don't do that" one it must be said.
22 hours ago [-]
nealabq 1 days 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.

toxik 1 days ago [-]
Not just for functional programmers. Prints and other I/O operations absolutely are side effects. That's not running counter to the point being made. Print in an assert and NDEBUG takes away that behavior.
nealabq 23 hours ago [-]
You're right of course. I was thinking specifically of printing log/debug statements in the assert(..), but that usually only happens if the assert(..) fails and exits, and in that case the "no side effects" rule no longer matters.
andrepd 23 hours 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 11 hours 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.

nyc_pizzadev 1 days 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.

BoingBoomTschak 1 days ago [-]
Yeah, but the macro system being so pitiful makes me long for one that allows something as magical as fiveam's is (https://github.com/lispci/fiveam/blob/e43d6c8e7da5a80d5c33e8...) instead of having to write special cases for unary and binary predicates.
omoikane 1 days 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.

nealabq 1 days ago [-]
In C++ you should probably #include <cstdio> instead of <stdio.h> unless you have a good reason. And especially avoid #including both. <cstdio> provides the function std::getc(..) while <stdio.h> usually provides getc(..) as a macro.

htons(..) and related socket-utility names are also often macros, but I'm pretty sure there is not a std::htons(..) in the C++ standard, partly because 'htons' is not an attractive name. Since it's (sometimes) a macro don't qualify its namespace like ::htons(..).

A long time ago in the Microsoft C (and later C++) dev envs there were macros named "min" and "max", which I thought were terrible names for macros.

adzm 1 days ago [-]
> A long time ago in the Microsoft C (and later C++) dev envs there were macros named "min" and "max", which I thought were terrible names for macros.

Yeah, this is still in windows.h unless you #define NOMINMAX

I remember having to guard against this in some inline code by surrounding the c++ calls with parenthesis, eg `(std::min)(a, b)`

nananana9 1 days ago [-]
Yep. There's tons of others as as well. 16-bit x86 enjoyers will be happy to know there are `near` and `far` macros whose primary purpose in 2026 is to break my projection matrices. And of course every Win32 function that takes strings has a macro that resolves it to either the UTF-16 or ASCII variant, so your custom CreateWindow is now a CreateWindowA, tough luck buddy.

I usually wrap Windows.h in a header followed by 100 #undefs to contain the disease.

1 days ago [-]
grokcodec 1 days ago [-]
Friedns shouldn't let Freidns post on HN without running spell check
4 days ago [-]
adzm 1 days 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
amelius 1 days ago [-]
Shouldn't the preprocessor be fixed, if it trips that easily on common C++ constructs?
marginalia_nu 1 days ago [-]
Preprocessor is just doing text transformations on the sources.

It's not really something that can be fixed, other than moving away from the preprocessor and putting metaprogramming capabilities into the language itself (which C++ has been doing).

amelius 1 days ago [-]
I mean, you could extend it such that a simple comma has no special meaning.

But I agree, fewer special tricks is better and that includes the preprocessor.

1 days ago [-]
tom_ 1 days ago [-]
I'm sure the standardization committee are always looking for fresh ideas!
wpollock 23 hours 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.

puschkinfr 23 hours ago [-]
No, because the string will be implicitly converted to `true` and `(a && true) == a` (for boolean `a`), so it will only be `false` if the assertion fails. Using || would always evaluate to `true`
22 hours ago [-]
wpollock 21 hours ago [-]
You're right, thanks!

This works too (but I wouldn't recommend it):

   assert( someBooleanExpression || ! "It is false" );
throwpoaster 1 days ago [-]
assert(spellcheck(“Friednly”));
nananana9 1 days ago [-]

  spellcheck.cpp:1:19: error: unexpected character <U+201C>
      1 | assert(spellcheck(“Friednly”));
        |                   ^
throwpoaster 19 hours ago [-]
Technically correct is best correct! Touché!
semiinfinitely 1 days ago [-]
"C++47: Finally, a Standard Way to Split a String by Delimiter"
MathMonkeyMan 11 hours ago [-]
There's been this since 1998, likely earlier:

    std::vector<std::string> split(const std::string& text, char delimiter) {
        std::vector<std::string> parts;
        std::istringstream stream(text);
        std::string part;
        while (std::getline(stream, part, delimiter)) {
            parts.push_back(part);
        }
        return parts;
    }
porise 1 days ago [-]
I'm still waiting for C++ to support Unicode properly.
einpoklum 1 days ago [-]
A standard way to split a string? Well, what's wrong with:

    std::views::split(my_string, delimeter)

?
nananana9 1 days ago [-]
Template bloat, terrible compile errors, terrible debug build performance, 1 second of extra compile time per cpp file when you include ranges, and you can't step through it in a debugger.
einpoklum 6 hours ago [-]
None of these complaints detract from this being a terse, readable, and available using the standard library, which is what the question was about...

I will agree that std::ranges is quite a jumble of templates, and has a compilation time penalty. Perhaps the use of modules will help with that somewhat.

fc417fc802 20 hours ago [-]
> you can't step through it in a debugger.

What do you mean by that?

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 20:18:38 GMT+0000 (Coordinated Universal Time) with Vercel.