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

(sandordargo.com)

68 points | by jandeboevrie 4 days ago

11 comments

  • WalterBright 1 day 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 1 day ago
      So you are ignoring our well beloved NDEBUG? :)

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

      • WalterBright 13 hours ago
        D compilers have a "ramming speed" setting.
  • MontagFTB 1 day 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 1 day 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 1 day 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 1 day 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 day 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 1 day 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 1 day 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 1 day 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 1 day 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 1 day 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 22 hours ago
                  What about rust const fn()? I think it guarantees there are no side effects
                  • IshKebab 21 hours ago
                    I think you're right. Equivalent to C++'s constexpr.
            • nmilo 1 day ago
              Compilers are very good these days. If it has no side effects it will likely be compiled out.
    • samiv 1 day ago
      That's why you define your own assert macro and keep in on unconditionally. Your programs will be better for it.
      • jandrewrogers 1 day 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 day 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 1 day 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 day 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 day 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 1 day ago
        Let's not vague post on HN. What's the problem with the above?
        • saagarjha 1 day 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 1 day 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 1 day ago
              Only if the assert is active. It basically means that the code is invalid when NDEBUG is set.
              • quuxplusone 19 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 1 day 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 1 day 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 1 day 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 1 day 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 1 day ago
              No, the code that does this is always active
              • aw1621107 1 day 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 1 day ago
                  Sorry, yes, I misread you
              • gottheUIblues 1 day 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 1 day 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 day 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 1 day 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 1 day ago
            Assert doesn't work like that in any language.
            • comex 1 day 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 1 day 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.

        • IshKebab 1 day ago
          There's nothing wrong with it. It does exactly what you think it does when passed null.
          • jmalicki 1 day 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 1 day 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 23 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 21 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 20 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 17 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 14 hours ago
                        That assert could completely fail to fire if inlined into another function that did a dereference first.
      • mhh__ 1 day ago
        This is a very "Dr Dr it hurts when I do this" "Don't do that" one it must be said.
    • nealabq 1 day 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 day 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 1 day 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 1 day 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 1 day 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 day 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 1 day 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 day 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 day 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 day 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.

  • grokcodec 1 day ago
    Friedns shouldn't let Freidns post on HN without running spell check
  • amelius 1 day ago
    Shouldn't the preprocessor be fixed, if it trips that easily on common C++ constructs?
    • marginalia_nu 1 day 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 day 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.

    • tom_ 1 day ago
      I'm sure the standardization committee are always looking for fresh ideas!
  • adzm 1 day 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 1 day 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 1 day 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`
      • wpollock 1 day ago
        You're right, thanks!

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

           assert( someBooleanExpression || ! "It is false" );
  • throwpoaster 1 day ago
    assert(spellcheck(“Friednly”));
    • nananana9 1 day ago

        spellcheck.cpp:1:19: error: unexpected character <U+201C>
            1 | assert(spellcheck(“Friednly”));
              |                   ^
      • throwpoaster 1 day ago
        Technically correct is best correct! Touché!
  • semiinfinitely 1 day ago
    "C++47: Finally, a Standard Way to Split a String by Delimiter"
    • MathMonkeyMan 1 day 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 day ago
      I'm still waiting for C++ to support Unicode properly.
    • einpoklum 1 day ago
      A standard way to split a string? Well, what's wrong with:

          std::views::split(my_string, delimeter)
      
      ?
      • nananana9 1 day 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 19 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 1 day ago
          > you can't step through it in a debugger.

          What do you mean by that?