I've done things like the following many times:
if (x != 5) {
... 40 lines here ...
} else {
assert(x == 5);
... more lines ...
}
The main purpose of the assert is to remind the reader that x == 5. There was a time where I would use a regular comment for a reminder like this, but since it's something the computer can check, I mine as well turn my regular comment into a checked comment. if (x != 5 && x != 6) {
... 40 lines here ...
} else {
assert(x == 5);
... more lines ...
}
Because of the 40 intervening lines the person might never see your assertion!If you work in a project with automated tests that do not have sufficient coverage, this is just a spectacular way of blowing up production.
Assertions are bad because they are checked at runtime which is too late. The ideal checked comment should have the checks happen at compile time. Something like Liquid Haskell.
I prefer what Java does: Assertions are disabled at runtime by default, however you can turn them on without requiring a different code-release and you can even do it for specific packages/classes.
So you might even use them in production: If feature X is already failing in some bizarrely uninformative way, turning on assertions in relevant code may provide you a better hint to what's going awry.
if (condition):
...
else:
assert(!condition)
...
When you could instead
if (condition):
...
else: #if (condition)
...Or otherwise explicitly comment the condition that was checked earlier. At least then if the comment is unchanged, whoever is touching your code will assume the comment is outdated, rather than assuming that you're asserting that x should be some undocumented magic number.
Great, I'd rather have this error cause a loud and obvious failure that can be easily hotfixed, than have it lead to subtly incorrect behavior that goes unnoticed for days and takes another day to debug.
Wouldn't that be nice! Seems like you're conflating two different classes of failures, though. There's no way to bypass runtime constraints entirely, though—all you can do is move it out of the production code and into tests, which takes a great deal of effort compared to an assert (which work just as well under tests, btw).
Also, until almost all the world's software has been rewritten in Idris, compile time checks just cannot do very much. This is a reality of our century.
Keep in mind that my original example isn't really about checking that x == 5. The focus is not on checking integer values, it's just an example. Although, again, even something as simple as checking integer values is beyond the compile time abilities of our compilers. Halting problem sends its regards!
You do not need to have the entire world's software rewritten in Idris (which I don't even like that much). This is purely local reasoning that can be accomplished by minor changes to existing languages if we want to. The compiler doesn't need access to the world. It is you who are unnecessarily raising the bar for what you expect compilers to do and then throwing in the towel just because the compiler can't actually do it.
Any argument that invokes the halting problem has a standard response: you don't need to handle every case; perfect is the enemy of good. Furthermore it is often better to have the programmer rewrite the code to help the compiler deduce these facts, because helping the compiler also helps future readers of the code.
I mean yea, it'd be great if programs propagated every possible error state or violated invariant possible up the stack. I don't see that happening any time soon, nor that being worth the effort. When was the last time you checked and propagated allocation failure? I'm guessing not in a long time, unless you're working with a runtime where allocation failure is explicitly handled as part of normal program progression (like the `alloc` libc family) and where you expect to run into memory limits.
Granted, exceptions are a good deal easier to handle "invisibly", but those have notably been absent from this discussion except in the case where they are unhandled as an assert might trigger.
[1] https://github.com/spc476/mod_blog/blob/master/src/backend.c...
Also isn't failing the assertion the whole point here? Without the assertion you would have a possibly hard-to-find bug rather than an assertion failure.
If you don't rely on `x == 5` in the else branch it's a bad idea, and too many of these spurious assertions will create a habit of just removing assertions whenever they fail.
If your devs and QA team (you do have QA and not just rely on automated tests I hope) don't trigger this assert *before* deployed to production then there's something seriously wrong with your development workflow.
After all, the code after that assert also assumes that the (now invalid) assumption that was checked by the assert is true. Without the assert telling you exactly what is wrong and where, you'd have a much bigger problem: some piece of code that now runs under wrong assumptions, and which may or may not cause much harder to diagnose problems further down the road.
Think of asserts as trip-wires, they are most useful at the entry of a function and prevent an error from propagating (because the earlier an error condition is caught, the easier it is to diagnose).
Or does QA switch to release builds at some point?
When asserts fail for QA, what do they see/collect?
That's just a convention, and IMHO not a good one. All the C++ games I helped shipping had asserts enabled in the release exe. We had our own custom assert macros, and different levels of assert checks (e.g. special "hot path asserts" were not included in release builds, but that was an exception and should be avoided - specifically, range-check asserts for array accesses were never removed since this was the most common source of problems). In general, the performance difference between keeping asserts in or removing them was mostly negligible (IIRC less than 2% in a typical frame), the only downside is that the executable becomes about 25% larger (and easier to reverse engineer) because of the embedded assert condition strings. This would have been solvable by replacing the assert strings with comptime hashes, but we didn't deem this important enough to implement).
In addition, the custom asserts also generated a stack trace and a mini dump for easier post mortem debugging.
> When asserts fail for QA, what do they see/collect?
A MessageBox with:
- the assert condition string
- filename and line number of the assert
- an optional printf-style programmer message (depending on the assert macro type)
- the pretty-printed function/method name which contains the assert (very useful for generic code, e.g. you don't just see `Array<T>::push()` but also the resolved template parameters)
- a stack trace (we generated a PDB file also for release builds to make the 'inhouse' stack traces human readable, but didn't ship the PDB to users - so stack traces for the shipped game would just display raw addresses, but those could be resolved on our side with the PDB we were storing for each release version - we wrote an extra inhouse tool for that)
- plus a mini dump is written for post-mortem debugging, but with all the information above that was hardly ever needed for figuring out what the problem was
The content of Windows MessageBoxes can be Copy-Pasted, and that's what QA was supposed to do when writing a ticket.
PS: also important to note that we didn't use the C++ stdlib (with very few exceptions, like std::sort), e.g. most importantly we wrote our own container and string types).
Except for writing your own stdlib. How long did that take? Was that to enhance bounds checking?
What I still do in my open source libraries is to allow overriding the assert macro so that people can integrate their own assert implementation (along with allowing to override memory allocation).
> Except for writing your own stdlib. How long did that take? Was that to enhance bounds checking?
TBH the main reason was that I think the C++ stdlib APIs have terrible ergonomics. We modelled our container classes after C# containers and some common sense. Full control over memory allocation and asserts was just a side effect.
The assertion didn't cause you to have limited test coverage that let you edit the branch condition without detecting that it introduced a bug in the false branch.
Yes, if you've got a language that supports compile time checking of complex invariants then that's awesome! But few languages support this today.
I also used to think it might be preferable to crash in some domains. With stories like https://news.ycombinator.com/item?id=37461695 it makes me think perhaps there are none.
In any case my main point is that assertions would be perfect if only they are checked at compile time.
For me assertions are for invariants. The code has no expectation of successful execution
I also like (borrowing from testing) the idea of expects, where we can hope that the object is in a specific state, but have an explicit compensation path for when it isn't, perhaps objects that need to be migrated to a new schema etc.
I feel that assertions are the first line of defense against programming judgement errors/omissions and are much easier to write in the given context than unit tests.
Of course in theory that last bit shouldn't make any difference, but then I'm sure you've already heard the old joke about the difference between theory and practice..
It's not a joke, it's a fact of life that we learn soon after graduating, and then relearn again, and again, and ...
In the extreme case, I can imagine ditching all your small-scale unit tests for a combination of asserts and end-to-end integration tests.
It's better to do both, asserts and tests are not mutually exclusive, they complement each other.
Presumably the assert is used because it doesn't consume resources (ie effort) to write. Testing takes a lot of effort.