C++20 is delivering some amazing new features around contracts - which for templates is going to make life much better - where constraints around types or other compile-time requirements can be baked into the template definition and enforced with decent diagnostics by the compiler. Yay!
However, I'm very concerned by the push towards terminating unconditionally when a runtime precondition violation occurs.
https://en.cppreference.com/w/cpp/language/attributes/contract
A program may be translated with one of two violation continuation modes:
off (default if no continuation mode is selected): after the execution of the violation handler completes, std::terminate is called; on: after the execution of the violation handler completes, execution continues normally. Implementations are encouraged to not provide any programmatic way to query, set, or modify the build level or to set or modify the violation handler.
I've written extensive user-facing software which traps all exceptions to a core execution loop where errors are logged and the user is informed of the failure.
In many cases, the user is better off saving if possible and exiting, but in many other cases, the error can be addressed by changing something in the design / data file they're working on.
This is to say - simply by altering their design (e.g. a CAD design) - the operation they wished to perform will now succeed. E.g. it's possible that the code was executed with too tight of a tolerance which micomputed a result based on that. Simply rerunning the procedure after changing tolerance would succeed (the offending precondition somewhere in the underlying code would no longer be violated).
But the push to make preconditions simply terminate and have no capacity to trap such an error and retry the operation? This sounds like a serious degradation in feature set to me. Admittedly, there are domains in which this is exactly desirable. Fail fast, fail early, and for preconditions or postconditions, the problem is in the way the code is written, and the user cannot remedy the situation.
But... and this is a big but... most software executes against an unknown data set that is supplied at runtime - to claim that all software must terminate and that there is no way that a user can be expected to rectify the situation seems to be to be bizarre.
Herb Sutter's discussion at the ACCU seems to be aligned strongly with the perspective that precondition & postcondition violations are simply terminate conditions:
https://www.youtube.com/watch?v=os7cqJ5qlzo
I'm looking for what other C++ pros are thinking from whatever your experiences coding informs you?
I know that many projects disallow exceptions. If you're working on one such project, does that mean you write your code to simply terminate whenever invalid input occurs? Or do you back out using error states to some parent code point that is able to continue in some way?
Maybe more to the point - maybe I'm misunderstanding the nature of the intent of C++20 runtime contracts are intended for?
Please keep this civil - and if your suggestion is to close this - perhaps you could be so kind as to point to a more appropriate forum for this discussion?
Most generally, I'm trying to answer, to my satisfaction:
How to check for and handle precondition violations (using best possible practices)?
It really comes down to this question: what do you mean when you say the word "precondition"?
The way you seem to use the word is to refer to "a thing that gets checked when you call this function." The way Herb, the C++ standard, and therefore the C++ contract system mean it is "a thing which must be true for the valid execution of this function, and if it is not true, then you have done a wrong thing and the world is broken."
And this view really comes down to what a "contract" means. Consider
vector::operator[]vs.vector::at().atdoes not have a precondition contract in the C++ standard; it throws if the index is out-of-range. That is, it is part of the interface ofatthat you can pass it out-of-range values, and it will respond in an expected, predictable way.That is not the case for
operator[]. It is not part of the interface of that function that you can pass it out-of-range indices. As such, it has a precondition contract that the index is not out-of-range. If you pass it an out-of-range index, you get undefined behavior.So, let's look at some simplistic examples. I'm going to build a
vectorand then read an integer from the user, then use that to access thevectorI built in three different ways:In case 1, we see the use of
at. In the case of bad input, we catch the exception and process it.In case 2, we see the use of
operator[]. We check to see if the input is in the valid range, and if so, calloperator[].In case 3, we see... a bug in our code. Why? Because nobody sanitized the input. Someone had to, and
operator[]'s precondition says that it is the caller's job to do it. The caller fails to sanitize its inputs and thus represents broken code.That is what it means to establish a contract: if the code breaks the contract, it's the code's fault for breaking it.
But as we can see, a contract appears to be a fundamental part of a function's interface. If so, why is this part of the interface sitting in the standard's text instead of being in the function's visible declaration where people can see it? That right there is the entire point of the contracts language feature: to allow users to express this specific kind of thing within the language.
To sum up, contracts are assumptions that a piece of code makes about the state of the world. If that assumption is incorrect, then that represents a state of the world that should not exist, and therefore your program has a bug in it. That's the idea underlying the contract language feature design. If your code tests it, it's not something you assume, and you shouldn't use preconditions to define it.
If it's an error condition, then you should use your preferred error mechanism, not a contract.