As others pointed out, assert
is kind of your last bastion of defense against programmer mistakes that should never happen. They're sanity checks that should hopefully not be failing left and right by the time you ship.
It's also designed to be omitted from stable release builds, for whatever reasons the developers might find useful: aesthetics, performance, whatever they want. It's part of what separates a debug build from a release build, and by definition a release build is devoid of such assertions. So there's a subversion of the design if you want to release the analogical "release build with assertions in place" which would be an attempt at a release build with a _DEBUG
preprocessor definition and no NDEBUG
defined; it's not really a release build anymore.
The design extends even into the standard library. As a very basic example among numerous, a lot of implementations of std::vector::operator[]
will assert
a sanity check to make sure you're not checking the vector out of bounds. And the standard library will start to perform much, much worse if you enable such checks in a release build. A benchmark of vector
using operator[]
and a fill ctor with such assertions included against a plain old dynamic array will often show the dynamic array being considerably faster until you disable such checks, so they often do impact performance in far, far from trivial ways. A null pointer check here and an out of bounds check there can actually become a huge expense if such checks are being applied millions of times over every frame in critical loops preceding code as simple as dereferencing a smart pointer or accessing an array.
So you're most likely desiring a different tool for the job and one that isn't designed to be omitted from release builds if you want release builds that perform such sanity checks in key areas. The most useful I personally find is logging. In that case, when a user reports a bug, things become a lot easier if they attach a log and the last line of the log gives me a big clue as to where the bug occurred and what it might be. Then, upon reproducing their steps in a debug build, I might likewise get an assertion failure, and that assertion failure further gives me huge clues to streamline my time. Yet since logging is relatively expensive, I don't use it to apply extremely low-level sanity checks like making sure an array is not accessed out of bounds in a generic data structure. I use it in more high-level contexts with more information specific to the domain of the application.
Yet finally, and somewhat in agreement with you, I could see a reasonable case where you might actually want to hand testers something resembling a debug build during alpha testing, for example, with a small group of alpha testers who, say, signed an NDA. There it might streamline the alpha testing if you hand your testers something other than a full release build with some debugging info attached along with some debug/development features like tests they can run and more verbose output while they run the software. I have at least seen some big game companies doing things like that for alpha. But that's for something like alpha or internal testing where you're genuinely trying to give the testers something other than a release build. If you're actually trying to ship a release build, then by definition, it should not have _DEBUG
defined or else that's really confusing the difference between a "debug" and "release" build.
Why has this code to be removed before release? The checks are not
much of a performance drain and if they fail there is definitely a
problem that I'd prefer a more direct error message about.
As pointed out above, the checks are not necessarily trivial from a performance standpoint. Many are likely trivial but again, even the standard lib uses them and it could impact performance in unacceptable ways to many people in many cases if, say, random-access traversal of std::vector
took 4 times as long in what's supposed to be an optimized release build because of its bounds checking that's never supposed to fail.
In a former team we actually had to make our matrix and vector library exclude some asserts in certain critical paths just to make debug builds run faster, because those asserts were slowing down the math operations by over an order of magnitude to the point where it was starting to require us to wait 15 minutes before we could even trace into the code of interest. My colleagues actually wanted to just remove the asserts
outright because they found that just doing that made a whopping difference. Instead we settled on just making the critical debug paths avoid them. When we made those critical paths use the vector/matrix data directly without going through the bounds-checking, the time required to perform the full operation (which included more than just vector/matrix math) reduced from minutes down to seconds. So that's an extreme case but definitely the asserts are not always negligible from a performance standpoint, not even close.
But also it's just the way asserts
are designed. If they didn't have such a huge performance impact across the board, then I might favor it if they were designed as more than a debug build feature or we might use vector::at
which includes the bounds checking even in release builds and throws on out of bounds access, e.g. (yet with a huge performance hit). But currently I find their design a lot more useful, given their huge performance impact in my cases, as a debug-build-only feature which is omitted when NDEBUG
is defined. For the cases I've worked with at least, it makes a huge difference for a release build to exclude sanity checks that should never actually be failing in the first place.
vector::at
vs. vector::operator[]
I think the distinction of these two methods gets at the heart of this as well as the alternative: exceptions. vector::operator[]
implementations typically assert
to make sure that out of bounds access will trigger an easily-reproducible error when trying to access a vector out of bounds. But the library implementers do this with the assumption that it won't cost a dime in an optimized release build.
Meanwhile vector::at
is provided which always does the out of bounds check and throws even in release builds, but it has a performance penalty to it to the point where I often see far more code using vector::operator[]
than vector::at
. A lot of the design of C++ echoes the idea of "pay for what you use/need", and a lot of people often favor operator[]
, which doesn't even bother with the bounds checking in release builds, based on the notion that they don't need the bounds checking in their optimized release builds. Suddenly if assertions were enabled in release builds, the performance of these two would be identical, and usage of vector would always end up being slower than a dynamic array. So a huge part of the design and benefit of assertions is based on the idea that they become free in a release build.
release_assert
This is interesting after discovering these intentions. Naturally everyone's use cases would be different, but I think I would find some use for a release_assert
which does the check and will crash the software showing a line number and error message even in release builds.
It would be for some obscure cases in my case where I don't want the software to gracefully recover as it would if an exception is thrown. I would want it to crash even in release in those cases so that the user can be given a line number to report when the software encountered something that should never happen, still in the realm of sanity checks for programmer errors, not external input errors like exceptions, but cheap enough to be done without worrying about its cost in release.
There are actually some cases where I would find a hard crash with a line number and error message preferable to gracefully recovering from a thrown exception which might be cheap enough to keep in a release. And there are some cases where it's impossible to recover from an exception, like an error encountered upon trying to recover from an existing one. There I'd find a perfect fit for a release_assert(!"This should never, ever happen! The software failed to fail!");
and naturally that would be dirt cheap since the check would be performed inside an exceptional path in the first place and wouldn't cost anything in normal execution paths.