Let's say you have a header lib.h:
inline int foo(int i) {
assert(i > 0);
//...
}
In C, this function is unspecified behavior that will probably work if the compiler is remotely sane.In C++, including this in two C++ translation units that set the NDEBUG flag differently creates an ODR violation. The C++ solution to this problem was a system where each translation unit enforces its own pre- and post- conditions (potentially 4x evaluations), and contracts act as carefully crafted exceptions to the vast number of complicated rules added on top. An example is how observable behavior is a workaround for C++ refusing to adopt C's fix for time-traveling UB. Lisa Lippincott did a great talk on this last year: https://youtu.be/yhhSW-FSWkE
There's not much left of Contracts once you strip away the stuff that doesn't make sense in C. I don't think you'd miss anything by simply adding hygenic macros to assert.h as the author here does, except for the 4x caller/callee verification overhead that they enforce manually. I don't think that should be enforced in the standard though. I find hidden, multiple evaluation wildly unintuitive, especially if some silly programmer accidentally writes an effectful condition.
I think these conditions should be part of the type signature, different to what was suggested in the otherwise good talk you cited.
In general though, the compiler can't optimize across the translation unit boundary without something like LTO. The code for the callee might have already been generated by the time the caller sees that the precondition is statically satisfied.
This is a huge challenge for a C-like language with pervasive global state. Might be more feasible for something like Rust, but still very difficult.
I find the general concept incredibly useful, and apply it in the more general sense to my own code, but there's always a bit of "what do I actually want contracts to mean / do here" back-and-forth before they're useful.
PS: I do like how D does contracts; though I admit I haven't used D much yet, to my great regret, so I can't offer my experience of how well contracts actually work in D.
A good contract system may in fact rely on type-safety as part of its implementation, but types do not necessarily cover all aspects of contracts (unless you're referring to the full gamut of theoretical generalisations of things like dependent types, substructural types, constraint logic programming, etc), and are also not necessarily limited to things that only apply at compile-time.
I think that implementations trying out their own experimental features is normal and expected. Ideally, standards would be pull-based instead of push-based.
The real question is what prevented this feature from being proposed to the standardization committee.
But if I want to use Eiffel, I’ll use Eiffel (or Sather).
I’d rather C remained C.
Maybe that’s just me?
C especially was designed with lots of security defects, and had it not been for UNIX being available for free, it would probably never taken off.
Most C developers don't want a modern C, they want a reliable C. WG14 should be pushing for clarifications on UB, the memory and threading model, documenting where implementations differ, and what parts of the language can be relied and what not.
Nobody really needs a new way to do asserts, case ranges, or a new way to write the word "NULL".
Your question can be reflected back to you: if you want an ever changing languages, go to Java, C# or C++, why mess with C?
f(int n, int a[n])
Actually do what it looks like it does. Sigh
What new clang feature are you talking about?
You're welcome!
https://en.cppreference.com/w/cpp/container/span.html
Or if you want multidimensional span:
from reading about contracts for C before i assumed it would be like what cake[1] does, which actually compile time enforces pointer (non)nullability, as well as resource ownership and a bunch of other stuff, very cool project, check it out if you haven't seen it yet :)
#define contract_assume(COND, ...) do { if (!(COND)) unreachable(); } while (false)
But this means that the compiler is allowed to e.g. reorder the condition check and never output the message. (Or invoke nasal demons, of course).This doesn't make much sense. I get that you want the compiler to maybe do nothing different or panic after the assertion failed, but only really after triggering the assertion and the notion of after doesn't really exist with undefined behaviour. The whole program is simply invalid.
To the brain of a compiler writer UB means "the standard doesn't specify what should happen, therefore I can optimize with the assumption UB never happen." I disagree that this is how UB should be interpreted, but this fight is long lost.
With that interpretation of UB, all `unreachable()` means is that the compiler is allowed to optimize as if this point in the code will never be reached. The unreachable macro is standard in C23 but all major compilers provide a way to do it, for all versions of the language.
So if you have a statement like `if (x > 3) unreachable()` that serves as both documentation of the accepted values, as a constraint that the optimizer can understand - if x is an unsigned int, it will optimize with the assumption that the only possible values are 0,1,2.
Of course in a debug build a sane compiler would have `unreachable()` trigger an assert fail, but they're not required to, and in release they most definitely won't do so, so you can't rely on it as a runtime check.
Exactly. But we already have unreachable and assert. The whole point of contracts is, that they are checked by the compiler (when the compiler invoker asks for it).
Having the contract invoke UB in the fail case means that instead of replacing the error return with a diagnostic provable by the compiler, you replace the error return with potential corruption. In which case is that ever the right choice?
I cannot, in good conscience, use a technology that adds even more undefined behavior. Instead it reinforces my drive to avoid C whenever I can and use OCaml or Rust instead.
It's also a good thing to tell the compiler that the programmer intends that this case will never happen, so that the static analyzer can point out ways through the code, where it actually does.
Given the examples, the author wants to ensure that 0 is not a possible input value, and NULL is not a possible output value.
This could be achieved with a simple inline wrapper function that checks pre and post conditions and does abort() accordingly, without all of this extra ceremony
But regardless of the mechansim you're left with another far more serious problem: You've now introduced `panic` to C.
And panics are bad. Panics are landmines just waiting for some unfortunate circumstance to crash your app unexpectedly, which you can't control because control over error handling has now been wrested from you.
It's why unwrap() in Rust is a terrible idea.
It's why golang's bifurcated error mechanisms are a mess (and why, surprise surprise, the recommendation is to never use panic).
But these contracts don't make things better.
Now you're removing control from the user. So now if an allocation fails, you crash. No way to recover from it. No getting an error signal back (NULL) so that you can say "OK, I need to clear up some memory and then try again". (Note that I'm not saying that inline error signaling such as NULL is good design - it's not).
Nope. No error handling. No recovery. You crash. And ain't nothing you can do about it.
That's just bad design on top of the existing bad design. Things that crash your app are bad. No need to add even more.