Skip to content(if available)orjump to list(if available)

Contracts for C

Contracts for C

32 comments

·September 5, 2025

AlotOfReading

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.

WalterBright

Digital Mars C++ has had contracts since, oh, the early 1990s?

https://www.digitalmars.com/ctg/contract.html

motorest

> Digital Mars C++ has had contracts since, oh, the early 1990s?

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.

arunc

It was proposed by Walter and denied by Stroustroup, probably to save C++. Karma hits back and he is trying to save C++ from Rust.

pjmlp

People keep forgetting C++ design is driven by 300+ people, and the features that get into the language go to elections, that they have to win.

Stroustoup has one vote, not everything he advocates for wins votes, including having a saner C++ (Remember the Vasa paper).

sirwhinesalot

Contracts getting proposed but still no slice/span type or even standardization of that new clang feature that makes

f(int n, int a[n])

Actually do what it looks like it does. Sigh

pjmlp

Strange are the ways of security in WG14.

veltas

You can do f(int n, int (*a)[n]) and it does what it looks like, since C99.

https://godbolt.org/z/8dfKMrGqv

miropalmu

What do you mean no slice/span type?

https://en.cppreference.com/w/cpp/container/span.html

Or if you want multidimensional span:

https://en.cppreference.com/w/cpp/container/mdspan.html

sirwhinesalot

C, not C++. Also span had no bounds checking until the introduction of .at() in C++26, which was a very silly thing to do so late in an age where the white house was asking people to use memory safe languages.

taminka

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

__d

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?

veltas

Truly I agree, but if we can add features to improve C codebases without rewriting them then that's a win, and you can just ignore them if you don't like them (as I will), but to the people where this has benefit they can be used.

OCTAGRAM

Eiffel has unsolicited tracing garbage collection. For TGC-free programming there is Ada

jimbob45

Java 24 and C# 9 resemble little of their first versions. C++ might as well not even be the same language at this point. Why are we so conservative with C but then so happily liberal with every other language?

HexDecOctBin

People chose C because they liked C. Of course they don't want C to change. The only thing the ISO committee should be adding is stuff for filling in the holes in language (C23's Improved Tag Compatibility and __VA_OPT__ are good examples), not add features that were never part of C and were never supposed to be there.

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?

xboxnolifes

> People chose C because they liked C. Of course they don't want C to change.

The same thing can be said for every other language, yet they change.

brabel

This is funny because Java people say the same about Kotlin and Scala.

nananana9

The complexity of C# and C++ should be a warning, not something to strive towards. C++ has 3 reasonable implementations, C has hundreds, for all sorts of platforms, where you don't get anything else.

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

motorest

> The complexity of C# and C++ should be a warning, not something to strive towards.

I think this talk about "complexity" is a red herring. C++ remains one of the most popular languages ever designed, and one of the key reasons is that since C++11 the standardization effort picked up steam and started including features that the developer community wanted and was eager to get.

I still recall the time that randos criticized C++ for being a dead language and being too minimalistic and spartan.

> C++ has 3 reasonable implementations, C has hundreds, for all sorts of platforms, where you don't get anything else.

I don't understand what point you are trying to make. Go through the list of the most popular programming languages, and perhaps half of them are languages which only have a single implementation. What compelled you to criticize C++ for having at least 3 production-quality implementations?

> Most C developers don't want a modern C, they want a reliable C.

You speak only for yourself. Your personal opinion is based on survivorship bias.

I can tel you that as a matter of fact a key reason why the likes of Rust took off was that people working with low-level systems programming were desperate for a C with better developer experience and sane and usable standard library.

> Nobody really needs a new way to do asserts, case ranges, or a new way to write the word "NULL".

Again, you speak for yourself, and yourself alone. You believe you don't need new features. That's fine. But you speak for yourself.

lifthrasiir

C++ historically had much more implementations. It is probably more about the availability of quality compilers that are free to use and adapt, because even in C most new compilers for niche platforms are now based on GCC or clang for the practical reason.

dmitrygr

Because there must be at least one refuge for the sane

unit149

[dead]

kstenerud

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

integricho

Though is there a significant difference in which is more bad between running into undefined behavior and panic?

kstenerud

In C, sure. C is a dangerous language of its time.

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.

hdjrudni

I'm not convinced. If something is going to cause a crash, like a nullptr, I'd rather crash near where the error happened with a nice error message, than hitting some UB crash god knows where.

Do I want my app to crash at all? No, of course not. If it's crashing, there's a serious bug. At least now I know where to look for it.

Should we pass back up an error signal instead of crashing? Yes, if it all possible, do that instead. Sometimes it's not possible or not worth the hassle for something you're 99.99999% sure can't/won't happen. Or literally can't currently happen, but you're afraid someone on the project might do a bad refactor at some point 5 years down the road and you want to guard against some weird invariant.

ost-ing

Exactly, panicking is a safer way to handle the situation rather than memory access violations

AlotOfReading

Safer in what sense? We have no idea whether this hypothetical code is in a userspace application that can exit safely at any time or a hard real time system where panicking could destroy hardware.

A lot of important programs (like the Linux kernel) don't operate strictly on the exact letter of the standard's UB semantics. They do things like add compiler flags to specify certain behaviors, or assume implementation details.