16 comments

[ 3.0 ms ] story [ 38.4 ms ] thread
I like Eiffel.

But if I want to use Eiffel, I’ll use Eiffel (or Sather).

I’d rather C remained C.

Maybe that’s just me?

Are you being really honest with yourself that you would renounce c90, c99 additions to stay with the original language as it was introduced in K&R C book?
do i understand correctly that there's nothing preventing someone from adding a postcondition check X and then just not implementing it inside the function? wouldn't this just mean that now the ub is triggered by the post()'s `unceachable()` instead of whatever ub would happen w/, say, dereferencing a null pointer, as a consequence of not actually implementing post check X? so it's just for speed optimisations then?

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 :)

[1]https://github.com/thradams/cake

There's a bit of an impedence mismatch with Contracts in C because C++ contracts exist partially to rectify the fact that <cassert> is broken in C++.

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'm not sure I'm understanding this correctly...

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).

The author writes that contract_assume invokes undefined behaviour when the assertion fails:

    #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.

> Here unreacheable() is the new macro from C23 (and C++23) that makes the behaviour undefined whenever the branch of the invocation is reached.

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.

For what it's worth, Rust has std::hint::unreachable_unchecked() which does exactly the same thing.
One of the biggest problems I find with contracts whenever contracts are mentioned is that nobody seems to have a really clear definition of what exactly a contract 'is' or 'should be' (with the exception of languages where contracts are a formal part of the language, that is).

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.

(comment deleted)