And this is why I'm on the fence about how I should approach error handling. If you even use a single assert (without a matching if check to exit the function or otherwise handle the problem) to eliminate one possible code path in a Debug build, your code is technically broken (or has a bug, at least) in Release builds.
That depends on how you use assert. Breaches of contract are "impossible", so if you check the contract of a function, assert should never ever fire. If it does, you're in serious trouble, since the impossible just happened.
Asserts are not for error handling, they are for checking of basic sanity (as you almost never get complete coverage).
If you have a breach of sanity in a release build, you're doomed already, one more code path is not going to make a difference other than not catching a crash-in-progress early.
Checking error returns of a function call by assert makes no sense in general. If an function can fail, it will at some point (else, what use is the error return?). As SmkViper said, unless you can continue without whatever the function was doing, there is basically only one solution, namely die with a nice error message.
In particular, never do
assert(f(a, b, c) != 0);
In a release, the assert is removed, and with it, any side effect that it does, like performing the function call!
Maybe what I should also ask is that in what cases should you: only use asserts, use asserts and some appropriate if checks*, or use asserts and if check* absolutely everything?
Not checking and acting on return values from function calls is an open door to problems. You call the function for a reason. If it fails, you didn't get what you wanted. You have to cope with it.
How to check depends on how "impossible" the condition is.
- If a user needs to know of the problem, produce an error.
- Then decide how to go on.
- If sanity is at stake, use an assert (you're doomed already).
- If you cannot continue, a crash or exit is the only way left
- otherwise, move on.
Note that user report and deciding the next step are separate things. If you do report to the user, make sure you phrase the problem in user-terms. "Cannot find foo.dat" has no meaning for a user, "Tried loading '/path/to/the/foo.dat' file, but could not find it" has that little extra information where a user gets a handle on fixing the problem. It usually means extra work in programming, but it's very nice, and it works (even for yourself).
In the end, bad code will eventually crash no matter what. The question is how quickly can you catch it. In general, quicker is better. It reduces development time, and increases code quality. Always explicitly test all cases, including the last one (if (x == 0) {...} else { assert(x == 1); ... }). Always add a 'default: crash("Oops");' in your switch. Verify post-conditions if some code to compute it is tricky, eg assert(data[computed_value] == nullptr); . Reduce the space you work in as much as possible, try to catch as many false moves as you can.
On the other hand, don't go overboard:
x = 1;
assert (x == 1); // duh!
or
index = -1;
for (i = 0; i < N; i++) if (d[i] == 5) { index = i; break; }
assert(index < 0 || d[index] == 5);
There is a balance here. Code that you know will work does not benefit from a check.
Last but not least, all that you do in this area is just damage control. If you write perfect code, you wouldn't need any of it. Unfortunately, very few people write perfect code, and we end up adding more code (with more mistakes) to catch errors. So while it's useful to let errors not escape to reduce impact, your primary aim should be in writing correct code.