Contracts for C

(gustedt.wordpress.com)

107 points | by joexbayer 6 days ago ago

113 comments

  • AlotOfReading 2 days ago ago

    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.

    • 1718627440 2 days ago ago

      I think the main point of pre- and post-conditions is, that the compiler can see them and prove that they match and will never be triggered. There probably will be a compiler flag for outputting all non-proved pre/postconditions, there is already -fanalyzer.

      I think these conditions should be part of the type signature, different to what was suggested in the otherwise good talk you cited.

      • AlotOfReading 2 days ago ago

        Adding conditions to the type signature would be an ABI breaking change in C++ and have nasty interactions with templates.

        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.

        • 1718627440 2 days ago ago

          My suggestion was for C where types are for example not encoded in the name, so I thought it only matters for type checking and optimization.

          > In general though, the compiler can't optimize across the translation unit boundary

          Which is why I would put it in the function signature, so it is available in both translation units. Making the code match the function signature is currently generally the responsibility of the caller. For example when I declare an argument of type double and write an integer in the call, the compiler will convert it to a double on the callers side. I think the safety story will be similar to a printf-call today. A dumb compiler does nothing, the smart compiler adds a warning/error, when the precondition fails.

          My understanding is, that on the callee's side this case is simply undefined behaviour. Much like it is today for example, that you can't pass a NULL everywhere, it might be declared to be UB, but currently this is only documented and internal to the callee and not documented in the function signature.

          PS: This does not conflict with my other comment (https://news.ycombinator.com/user?id=1718627440), that this can't be implemented as a macro that invokes UB:

              callee (void * p, [...])
              contract_assume (nonnull (p), "p can't be NULL!")
              {
                  p[0] = foo; -> UB
              }
          
          The access through p simply becomes UB like it always was. But the contract_assume, can't be UB, since then the check and diagnostic is omitted or reordered.
          • AlotOfReading 2 days ago ago

            The ABI thing is because Lisa's talk was about C++. In C, a function can have multiple declarations as long as they're compatible (as opposed to identical).

            So these declarations might coexist without issue even though they have different signatures:

               extern int foo(a, b); // in include/lib.h
               int foo(int a, int b); // in src/foo.h
            
            whereas this would be incompatible

               const int foo(int a, int b); // "nearly" compatible
            
            If you attach things to the prototype, then you need to sort out the compatibility rules. If contract_assume(a > 0) changes the type, the extern shouldn't be compatible. This is frequently used to allow linking against libraries compatible with older language standards while allowing newer code to benefit from newer standards like C99, C11, or C23.

            The C23 committee ran into this issue when introducing attributes. Their solution was just exclude attributes from the signature and say they're always compatible:

                Although semantically attached to a function type, the attributes described are not part of the prototype of such a function, and redeclarations and conversions that drop such an attribute are valid and constitute compatible types.
            • 1718627440 2 days ago ago

              Maybe I'm missing something, but how does this change anything to how it's now?

              I can happily declare two completely incompatible functions with the same symbol name, as long as they are in separate TUs and I don't use -flto, neither the compiler nor the linker will complain and my program will simply be garbage. This won't change with incompatible contracts.

              When I both show them to the compiler, when they contradict, the compiler will complain, that also doesn't change.

              Of course this will not work:

                  extern int foo(a, b);
                  int foo(int a, int b) contract_assume(a > 0);
              
              However this will:

                  extern int foo(a, b) contract_assume(a > 0);
                  int foo(int a, int b);
              
              But this isn't a problem, since this is precisely the feature we want to introduce contracts for: catching function call mismatches that are not yet expressible in the language.

              > while allowing newer code to benefit from newer standards

              Having no contract specified should of course result in no additional restrictions being exposed beside this already present now. This wouldn't be possible:

                 foo(unsigned int a) contract_assume(possible(a < 0))
              
              But I don't think anybody is arguing for that.
      • zozbot234 2 days ago ago

        > the compiler can see them and prove that they match and will never be triggered

        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.

        • 1718627440 2 days ago ago

          You only need to check between caller and callee. If their constraints always match, it can't be triggered, if they contradict always, it will be triggered, else it can't be decided and the programmer can add annotations if he cares about, or the compiler can check what it would be like if the caller is inlined into his calling functions if that is trivial enough.

  • tpoacher 2 days ago ago

    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.

    • bmn__ 2 days ago ago

      No wonder it looks less than awesome to you. A contract is just a hack. Ideally, it should not exist because the type system already covers the programmer's intent. Languages that have shitty types which cannot express very much must work around the problem with contracts.

      • tpoacher 2 days ago ago

        Partially agree, but only for a very narrow definition of what is a contract, which again is the problem stated above.

        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.

      • 1718627440 2 days ago ago

        Aren't contracts a feature of the type system? They encode a specific type that differs from the base type by a more complex predicate, like a constraint check in SQL.

  • WalterBright 2 days ago ago

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

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

    • motorest 2 days ago ago

      > 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 2 days ago ago

        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.

        • WalterBright 2 days ago ago

          D is the result of lack of interest by the C++ committee, and I had little interest in spending literally years trying to get useful things adopted into C++.

          Ironically, over the years, C++ has adopted many features popularized by D.

          (like contracts!)

          C++ should adopt a few more D features, like https://www.digitalmars.com/articles/C-biggest-mistake.html, compile time expression evaluation (C++ did it wrong), forward references, and modules that work. C++ should also deprecate the preprocessor, a stone-age kludge that has long been obsolete.

          • motorest 2 days ago ago

            > D is the result of lack of interest by the C++ committee, and I had little interest in spending literally years trying to get useful things adopted into C++.

            I think you are leaving out the fact that your comment applies to the post-C++98/pre-C++11 hiatus.

            Once C++11 was released, the truth of the matter is that whatever steam D managed to build up, it fizzed out.

            I'm also not sure if it's accurate to frame the problem with C++0x as picking up features from D. As I recall, D's selling point was that it was scrambling to provide the features covered by C++0x but users weren't forced to wait for a standard to be published to be able to use them. Once they could, there was no longer any compelling reason to bother with D anymore.

            • WalterBright 2 days ago ago

              C++ is still trying to catch up with:

              - compile time function execution

              - modules

              - no preprocessor

              - memory safe arrays

              - preprocessor replacement

              - ranges

              and so on.

              • motorest a day ago ago

                > C++ is still trying to catch up with (...)

                C++ modules are indeed a mess, but you are fooling yourself if you believe that the preprocessor of all things is a compelling reason to switch. In fact, I think you unwittingly proved my point on how interest in D fizzed out the moment C++11 was released.

                • WalterBright a day ago ago

                  > if you believe that the preprocessor of all things is a compelling reason to switch

                  The preprocessor is an unhygienic, ugly mess. Just look at the system .h files, which should be a showcase on how to use it correctly. I stand by my assessment of it.

              • 72deluxe a day ago ago

                As I am oblivious to D, may I ask if there are suitable GUI toolkits for it, or bindings? I typically use wxWidgets in C++ land.

        • pjmlp 2 days ago ago

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

        • motorest 2 days ago ago

          > It was proposed by Walter and denied by Stroustroup, probably to save C++.

          Citation needed.

          For starters, where is the paper?

          • shakna 2 days ago ago

            Well, there's this list Stroustrup offers, of systems in C++ that he would reject: [0]

            [0] https://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p0977r0...

            • pjmlp 2 days ago ago

              One vote, C++ isn't a BDFL driven language.

              Also WG14 famously killed Dennis Ritchie proposal to add fat pointers to C.

              Language authors only have symbolic value once they relish control to a standards body.

  • guerrilla 2 days ago ago

    I don't know what C++ is trying to do, but does everyone know about frama-c[1]?

    1. https://frama-c.com/

  • __d 2 days ago ago

    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?

    • johnisgood 2 days ago ago

      Ada / SPARK has contracts, too, that can be proven at compile-time. In fact, Ada alone suffices, it has pre- and post-conditions. I think Ada has more libraries than Eiffel does. Does anyone even write Eiffel? I am really curious if it is still alive anywhere, in some form.

      • WalterBright 2 days ago ago

        Eiffel failed largely because of licensing restrictions.

    • pjmlp 2 days ago ago

      Languages are software products like everything else in computing, either they evolve or they whither and die.

      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.

      • HexDecOctBin 2 days ago ago

        More likely they evolve AND they whither and die. The number of software I have stopped using due to bad updates is much higher than those with not enough updates.

    • veltas 2 days ago ago

      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.

    • jimbob45 2 days ago ago

      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?

      • nananana9 2 days ago ago

        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 2 days ago ago

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

          • babaceca 2 days ago ago

            The vast majority of C programmers will agree that they don't care for any of the new features, as is clearly evident by the fact that almost nobody elects to use the latest standards.

            The "most popular programming languages" are irrelevant here.

            C and C++ are standardized languages, and also the tools we use for code that actually matters. A standard that can't be implemented is worthless, and even the "3 high quality" implementations of C/C++ haven't fully implemented the latest 2 editions of either language.

            There's a lot more riding on these two languages than you give credit for, and they should be held to a higher standard. C is not the language to experiment with shiny new features, it's the language that works.

            > I can tel you that as a matter of fact a key reason why the likes of Rust took off

            So what's the problem? If Rust is gaining traction on C/C++, and people are excited about what it brings to the table, use it. We'll both do our thing, let it play out - we'll see which approach yields better software in 10 years.

            • motorest 2 days ago ago

              > The vast majority of C programmers will agree that they don't care for any of the new features,(...)

              I think this belief is based on faulty assumptions, such as survivorship bias.

              C++ became popular largely because it started off by extending C with the introduction of important features that the developer community wanted to use. The popularity of C++ over C attests how much developers wanted to add features to C.

              C++ also started being used over C in domains where it was not an excellent fit, such as embedded programming, because the C community prefered to deal with C++'s higher cognitive load as an acceptable tradeoff to leverage important features missing from C.

              The success of projects such as Rust and even Zig, Nim also comes at the expense of C's inability to improve the developer experience.

              Not to mention the fact that some projects are still developed in C because of a mix of inertia and lack of framework support.

              So to claim that the C programmers do not want change, first you need to ignore the vast majority that do want but already dropped C in favor of languages that weren't frozen in time.

              It's also unbelievable to claim that a language that precedes the concept of developer experience represents the apex of language design. This belief lands somewhere between Stockholm syndrome and being mentally constrained to not look beyond a tool.

              • WalterBright 2 days ago ago

                C++ became popular because in the late 80s, 90% of programming was done on the PC. Zortech C++, with the first native compiler, provided the most powerful metal programming language available.

                Zortech C++ is what gave C++ critical mass to succeed.

                P.S. before ZTC++, the traffic in the usenet C++ newsgroup was neck-and-neck with the objective C newsgroup. After ZTC++ was released, traffic in the C++ newsgroup took off and the objective C one faded away. Borland saw our success, and pivoted away from their nascent attempt at an OOP language towards implementing Borland C++. Microsoft then also abandoned their OOP C project (called C) in favor of developing C++.

                (I've never been able to get any information about C, I was just told about it by a Redmondian.)

              • babaceca 2 days ago ago

                > So to claim that the C programmers do not want change, first you need to ignore the vast majority that do want but already dropped C...

                Good, we can ignore them. It's not a language for everybody, and if you're happily using C++, or Zig, or Nim, keep doing that.

                Developer experience is a weigted sum of many variables. For you cool syntax features may play a huge role of that, for most C programmers a simple language with clear and understandable semantics is much more important.

                There are many languages with cool syntax and shiny features, and very few of the latter kind. C belongs to the latter, and it also happens to be running a vast majority of the world's most important software.

                You keep bringing up Rust as an example. It's probably the most famous of the new-age systems languages. If it's such a great language, when will we see a useful program written in it?

                • motorest 2 days ago ago

                  > Good, we can ignore them.

                  Who do you think you're representing? At best you only speak for yourself. It's perfectly fine if you choose to never update any tool you use, but that's just your personal opinion. You are free to stick with older standard versions of even compiler releases, but that is no justification to prevent everyone around you to improve their developer experience.

                  > It's not a language for everybody (...)

                  You might believe it isn't, but that's hardly a sane or rational belief.

                  • babaceca 2 days ago ago

                    Reality isn't on your side.

                      1. A lot of people use the old C standards.
                      2. Not a lot of people use the new ones.
                      3. A lot of useful software is written in C.
                      4. Not a lot of useful software is written in any of the other languages you've listed in this conversation, despite the fact that you can hardly call them "new" at this point.
                    
                    I'm done with you, I'll leave you to puzzle out the obvious conclusion of these 4 points.

                    You write software your way, I'll write it mine, and in 10 years we can check our homework. The first 10 years of Rust haven't really given us any results software-wise, but I'm sure with language design powerhouses such as yourself on the case, and just a few more pieces of syntax sugar, you can turn it around.

                • aw1621107 2 days ago ago

                  > If it's such a great language, when will we see a useful program written in it?

                  I think it should have been simple enough to find examples, though I suppose there might be some dependence on what you mean by "useful".

                  For standalone stuff, some examples might be Ripgrep, ruff, uv, Alacritty, and Polars. Rust is also used internally by some major companies, such as Amazon, Dropbox, Mozilla, Microsoft, Google, Volvo, Discord, and CloudFlare.

                  • babaceca 2 days ago ago

                    > there might be some dependence on what you mean by "useful".

                    I should've been clearer about that, but what I mean by that is pretty much what a normal non-technical person would consider an useful piece of software - Photoshop, Figma, Excel, Chrome, Windows, Android, Blender, AutoCAD, Unreal Engine, any Office Suite...

                    Since this is a technical forum I think we'd both easily agree on a bunch of very technically impressive software that the average person hasn't heard of - ffmpeg, qemu, LLVM, Linux, Postgres, V8, etc.

                    It would be a stretch to put any of the tool on either of those lists. Given the popularity of Rust, and that it's now over 10 years old, I'd expect at least one major program that can serve as an example of "here's this very useful, complex software package, as proof that our methodology works and you can do cool things this way."

                    • aw1621107 a day ago ago

                      > but what I mean by that is pretty much what a normal non-technical person would consider an useful piece of software

                      That seems like an... interesting... definition of "useful" to me. Why that definition?

                      > Photoshop, Figma, Excel, Chrome, Windows, Android, Blender, AutoCAD, Unreal Engine, any Office Suite...

                      To be fair, there is Rust in Windows and Android, and IIRC there's movement towards using it in Chrome as well.

                      > It would be a stretch to put any of the tool on either of those lists.

                      OK, but your second list has a different set of qualifications than the first. You originally just asked for "useful" programs, and that's what you asked for in the original comment I responded to. Now it's "very technically impressive". So which do you want?

                      I feel like it's probably not a bad idea to ask exactly what you mean by "technically impressive" as well, since I think it's hard to argue that ripgrep and polars don't at least have technically impressive parts in them.

                      > Given the popularity of Rust, and that it's now over 10 years old, I'd expect at least one major program that can serve as an example of "here's this very useful, complex software package, as proof that our methodology works and you can do cool things this way."

                      That seems like a bit of a questionable metric to me.

                      Given that Rust was explicitly designed and intended to be incrementally adoptable in existing codebases, it doesn't make sense to me to solely look for standalone programs since incremental adoption is very much part of "our methodology". This also sort of ties into your expectation in the first place - I'm not sure I'd expect the same given Rust's design and niche, as well as the general software landscape now vs. when C/C++ were a similar age.

                    • burntsushi a day ago ago

                      Classic example of moving the goalposts. You'll keep doing it no matter what examples people give you.

            • aw1621107 2 days ago ago

              > The vast majority of C programmers will agree that they don't care for any of the new features, as is clearly evident by the fact that almost nobody elects to use the latest standards.

              I think there might be some mix between "we don't want any of the new features" and "we want the new features but can't". Probably hard to get good data on the precise split.

              > A standard that can't be implemented is worthless, and even the "3 high quality" implementations of C/C++ haven't fully implemented the latest 2 editions of either language.

              This is a bit black-and-white. Just because a standard isn't fully implemented doesn't mean the parts that are implemented can't be useful, especially if the "haven't implemented this yet" is more due to a lack of manpower/attention/desire/etc. than actual impossibility (e.g., libc++ and parallel algorithms/<charconv> vs. export template).

          • npoc 2 days ago ago

            I agree. A lot of new additions to C++ make coding with it simpler, not more complex. However it does mean there's more to learn, because the old style of the language still exists and is still accepted by the modern compilers as opposed to say, Rust which removes replaced language features when it releases a new edition.

            • pjmlp 2 days ago ago

              Language, not libraries.

              Editions are rather limited in what they support.

              Try to design a crate that stays compatible across editions, while using libraries that have changed signatures across editions.

              The crate itself keeps its own edition fixed.

        • lifthrasiir 2 days ago ago

          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.

          • pjmlp 2 days ago ago

            Both implementated in C++.

            As someone that remembers the t-shirts with "my compiler compiles yours" that some C folks used to wear, it is kind of ironic having that turned around on them.

        • pjmlp 2 days ago ago

          Most single C implementations have died out.

          There is hardly any C compiler worth using that isn't equally a C++ compiler .

          In fact, there is any C compiler left worth using that hasn't been rewriten into C++.

          • aninteger 2 days ago ago

            There seems to be at least 2 lcc forks (lcc-win32/win64, and pelles-c) that have their own small communities. Although maybe they are dead.

            • pjmlp 2 days ago ago

              Very tiny small communities, what product is being shipped with them?

          • WalterBright 2 days ago ago

            My current C compiler is implemented in D :-)

            • pjmlp 2 days ago ago

              Plus points for BetterC.

          • gustedt 2 days ago ago

            That may be true in your bubble, but I don't think this is true in general. There are still a lot C compilers out there, compared to 3 or 4 that do C++.

            • pjmlp 2 days ago ago

              Other than legacy embedded CPUs like PICs, I doubt it.

        • WalterBright 2 days ago ago

          Check out all the extensions added to C, all incompatible with each other.

      • HexDecOctBin 2 days ago ago

        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 2 days ago ago

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

          • WalterBright 2 days ago ago

            A common thing people write about D is don't add any more features except for their feature proposals. :-)

          • oguz-ismail 2 days ago ago

            Well they shouldn't have. It was much more fun to program in Python 2.7 and Java 7, I wouldn't touch those languages with a five feet pole these days

            • lifthrasiir 2 days ago ago

              Why specifically Python 2.7 and not, like, 1.5? Python 1 and 2 are as different as 2 and 3 (that is, surprisingly not much).

        • pjmlp 2 days ago ago

          No, many people chose C, because they had to.

          • sarchertech 2 days ago ago

            Yeah but those people are running it on some platform where it’s the only choice. And they are probably running a subset of C99. The likelihood of new features ever making it to those platforms is close to zero.

            • pjmlp 2 days ago ago

              There is hardly any new C compiler that isn't C11.

              • sarchertech 2 days ago ago

                I’ve been out of embedded for a bit but last I checked almost nothing actually implemented all of C11?

                • pjmlp 2 days ago ago

                  There is hardly any compiler worth using that isn't a fork of either GCC or clang.

                  So unless they are stuck on a pre-historic fork, they support C11 as much as clang and GCC do.

                  Exception for stuff like PIC, Z80,...

                  Which didn't even support proper C on their glory days.

        • brabel 2 days ago ago

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

        • WalterBright 2 days ago ago

          C added the absurd normalized Unicode identifiers.

      • dmitrygr 2 days ago ago

        Because there must be at least one refuge for the sane

    • flykespice 2 days ago ago

      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?

    • OCTAGRAM 2 days ago ago

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

  • undefined 2 days ago ago
    [deleted]
  • sirwhinesalot 2 days ago ago

    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

    • uecker 20 hours ago ago

      You can just define a span type yourself: https://uecker.codeberg.page/2025-07-02.html

      What new clang feature are you talking about?

      • sirwhinesalot 8 hours ago ago

        -fbounds-safety

        • uecker 7 hours ago ago

          This works with new attributes as far as I know.

    • veltas 2 days ago ago

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

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

      • sirwhinesalot 2 days ago ago

        That's an entirely different thing (VLAs)

        • veltas 2 days ago ago

          There's no VLA in my example.

          • sirwhinesalot 2 days ago ago

            Your example doesn't do any bounds checks, it just lets you get the sizeof. And the reason the sizeof works is the VLA infrastructure (which is not supported by MSVC so it won't compile the code).

            What I want is -fbounds-safety from clang.

            • navi-desu 2 days ago ago

              it does do bounds checks if you -fsanitize=bounds, in gcc at least

              (and msvc is stuck on partial c11 support to this day, so imo, i don't quite think it's a fair target when comparing things to new features anyway)

    • WalterBright 2 days ago ago
    • miropalmu 2 days ago ago

      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 2 days ago ago

        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.

        • TuxSH 2 days ago ago

          > Also span had no bounds checking

          While there was no reason not to have .at(), lack of bound checks by default isn't a bad thing, as inlined bound checks have the potential to highly pessimize code (esp. in loops); also standard library hardening is a thing.

          IMO there's much more value to be had in migrating C code (and pre-C++11 code, too) to C++ (or Rust, depending on one's tastes); RAII - that is to say, the ability to automatically run destructors on scope exit - and in particular shared_ptr/unique_ptr/bespoke intrustive pointers drastically reduce the risks of use-after-free

          • sirwhinesalot 2 days ago ago

            I like how it's handled in Herb's cpp2/cppfront. If the type implements certain methods (like size()), and you turn on bounds checking, then the indexing operations (which are unsafe) are wrapped in i < c.size() ? c[i] : <throw or abort>.

            This way the indexing operation itself doesn't need to have bounds checks and it's easier for the compiler to optimize out the checks or for an "unchecked" section to be requested by the programmer.

            • aw1621107 2 days ago ago

              > I like how it's handled in Herb's cpp2/cppfront. If the type implements certain methods (like size()), and you turn on bounds checking, then the indexing operations (which are unsafe) are wrapped in i < c.size() ? c[i] : <throw or abort>.

              For what it's worth, something similar was proposed in the C++26 core profiles paper [0] and that particular bit got some pushback. From a reponse paper [1]:

              > The paper’s suggested approach includes adding bounds checking to containers solely based on a duck-typed deduction using the availability of operators and member functions without knowing for sure what purpose the container serves, what those members do, or even whether the class is, in fact, a container at all. [P3081]’s proposal would cause a wide variety of existing, perfectly valid code to fail unpredictably at run time:

              > — Containers that are not zero-indexed

              > — Sparse containers

              > — Map-like types the compiler cannot detect as being map-like

              > — A two-color image where size() is in bytes but for which indexing is by pixel (i.e., by bit)

              > — Ring-like containers

              > — Containers that automatically grow.

              > [P3081] also fails to consider that some library vendors already (and will continue in the future to) choose to put contract preconditions on their operator[], which would result in double and possibly inconsistent checking.

              [0]: https://wg21.link/P3081

              [1]: https://wg21.link/P3543

        • miropalmu 2 days ago ago

          Oh. Makes sense. My bad.

    • pjmlp 2 days ago ago

      Strange are the ways of security in WG14.

  • taminka 2 days ago ago

    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

  • 1718627440 2 days ago ago

    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.

    • babaceca 2 days ago ago

      There's no assertion required by spec.

      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.

      • 1718627440 2 days ago ago

        > 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?

  • unit149 2 days ago ago

    [dead]

  • tempodox 2 days ago ago

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

    • 1718627440 2 days ago ago

      I think it is good to explicitly invoke UB. It makes it much more obvious in the code, where it is intended and where not. It's a way to specify that this point in code is never reached, the code can't deal with it and I don't even care what the compiler does in this case.

      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.

    • aw1621107 2 days ago ago

      For what it's worth, Rust has std::hint::unreachable_unchecked() which does exactly the same thing.

  • kstenerud 2 days ago ago

    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 2 days ago ago

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

      • kstenerud 2 days ago ago

        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 2 days ago ago

          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.

          • 1718627440 2 days ago ago

            I think the point is about e.g. malloc as shown in the blog post. The stdlib function already have return values to indicate invalid arguments from the caller. These exist to allow the caller to decide what to do in this case. Replacing them by invoking UB or panicing, means the user looses control.

            Having the stdlib expose both ways, means either having two stdlibs (like on MS Windows), or dynamic checks in the stdlib. I don't think either way is a good idea. Thus, these contracts can't be used by libc.

            • kstenerud a day ago ago

              Precisely. The barn door is already open as far as the C library goes. And since C's error mechanism is all in-band (another bad idea in hindsight), the only thing the extra invariant checks will do is add even more (now uncontrollable) crash points to an already intolerably crash-prone app.

              Panics are like goto: Only useful in VERY rare circumstances. Every use of a panic should require a rock-solid justification for why you're choosing to crash the process - similar to how every call to abort() requires justification in any professional C codebase. assert() in production code was a horrible idea because liberal use was actually encouraged.

              Rust's panic mechanism was almost good. Unfortunately, they chose to give them all innocuous names like unwrap() rather than or_panic(). So now you have to check for it all the time using clippy because it's too easy for a human to accidentally gloss it over. Linting usually points to a design failure in the language UX.

          • bluGill 2 days ago ago

            The other hope for contracts is static whole program analysis can prove your program - at least in part. If you can prove some contract false that tells you where a bug is long before anyone triggers it.

      • ost-ing 2 days ago ago

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

        • AlotOfReading 2 days ago ago

          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.

          • imtringued 2 days ago ago

            I will never understand how C developers can catastrophize over Rust panics, a language that has a panicless "_try" version of every panic causing function that returns a Result instead, while simultaneously accepting the infinite growth of ever harder to avoid UB in C/C++ and telling people to never have undefined behavior in their code.

            If you think dealing with undefined behavior is easy and you assume that people have verified that their software triggers no undefined behavior at runtime is fair game, then you should grant that assumption in favor of Rust developers having done the same with their panics, because avoiding panics is child's play in comparison to avoiding UB.

            I don't know what it is about panics that triggers some mania in people. UB does not interrupt the program and therefore allows memory corrupt and complete takeover of a program and the entire system as a consequence. C developers are like "this is fine", while sitting in a house that is burning down.

            There used to be a pretty blatant hibernation bug with AMD GPUs on Linux that essentially crashes your desktop session upon turning your computer on from hibernation. I've also had a wifi driver segfault on login that forcibly logged you out so you couldn't login like 9 years ago. C doesn't magically fix these problems by not having an explicit concept of panics. You still need to write software that is correct and doesn't crash before you push an update.

            There is no meaningful difference between a correctness bug and a panic triggering condition with the exception that the panic forces you to acknowledge the error during development, meaning it is more likely that the correctness bug gets caught in the first place.

            • AlotOfReading 2 days ago ago

              Can we refrain from strawmen? I haven't made any of the points you're harpooning and vehemently disagree with all of them.

              What I said was that panics aren't always appropriate and the context to determine this doesn't exist at the language level.

              I didn't say managing UB was easy and in fact I've argued diagnosing it is impossible directly with members of both language committees. I didn't say panics are never appropriate. They usually are appropriate. I didn't say I don't use rust because X, Y, Z. I write rust. Etc.

                  There is no meaningful difference between a correctness bug and a panic triggering condition with the exception that the panic forces you to acknowledge the error during development, meaning it is more likely that the correctness bug gets caught in the first place.
              
              More likely, but not guaranteed. I don't want to engage more with you, but there was a specific incident I was thinking of when I wrote the prior post that involved an assert improperly placed in a real time control loop that burnt out a very expensive motor.
            • uecker 20 hours ago ago

              We removed 30% of UB in the core language already for C2y and are in progress of removing more, so there is no "infinite growth of ever hard to avoid UB". For many UB the right way is indeed to panic and in C you can often achieve this with the UB sanitizer (but not for all).