C++26: A User-Friednly assert() macro
sandordargo.com
sandordargo.com
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.
Our idea of declare (optimize (speed 3) (safety 0))
https://github.com/fiberfs/fiberfs/blob/7e79eaabbb180b0f1a79...
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...
The correct way to solve this is with debug asserts (as in Rust, or how the parent described).
`assert(vector.size() < 3)` is ridiculous to you?
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.
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/...
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.
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.
> 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.
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.
> 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.
> 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.
But yes, “assert” in most languages is debug-only.
> 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.
In this case, it may help to understand that e.g. border control enforces a traveler's permission to cross the border, then lets them proceed.
Only when NDEBUG is defined, right?
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.
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.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...
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.
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.
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.
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.
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.
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.
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)`
I usually wrap Windows.h in a header followed by 100 #undefs to contain the disease.
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).
But I agree, fewer special tricks is better and that includes the preprocessor.
Shouldn't that be "||" rather than "&&"? We want the message only if the boolean expression is false.
This works too (but I wouldn't recommend it):
assert( someBooleanExpression || ! "It is false" ); spellcheck.cpp:1:19: error: unexpected character <U+201C>
1 | assert(spellcheck(“Friednly”));
| ^ std::views::split(my_string, delimeter)
?What do you mean by that?
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.
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;
}