babel_ 2 days ago

Testing with Jen's macro this was based on, and found that the always_inline was redundant under even -O1 (https://godbolt.org/z/qoh861Gch via the examples from N3488 as became the baseline for the TS for C2y, which has recently a new revision under N3687), so there's an interesting trade-off between visibly seeing the `defer` by not not-inlining within the macro under an -O0 or similar unoptimised build, since with the inlining they are unmarked in the disassembly. But, there's an interesting twist here, as "defer: the feature" is likely not going to be implemented as "defer: the macro", since compilers will have the keyword (just `defer` in TS25755, or something else that uses a header for sugared `defer`) and may see the obvious optimised rewrite as the straightforward way of implementing it in the first place (as some already have), meaning we can have the benefit of the optimised inline with the opportunity to also keep it clearly identifiable, even in unoptimised and debug builds, which would certainly be nice to have!

scoopr 2 days ago

Then there is the proposal to add standard `defer` to C2y[0]

[0] https://thephd.dev/c2y-the-defer-technical-specification-its...

lionkor 2 days ago

Slightly off-topic, but:

The fact that go "lifts" the deferred statement out of the block is just another reason in the long list of reasons that go shouldn't exist.

Not only is there no protection against data-races (in a language all about multithreading), basically no static checking for safety, allocation and initialization is easy to mess up, but also defer just doesn't work as it does in C++, Rust, Zig, and any other language that implements similar semantics.

What a joke.

  • jibal a day ago

    One of those languages being D, which invented it (well, Andrei Alexandrescu did) under the name `scope(exit)` (there's also `scope(failure)` which is like Zig's `errdefer` and `scope(success)` which no other language has AFAIK).

majke 2 days ago

Nested functions are cool, although not supported by clang.

However they rely on Trampolines: https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html

And trampolines need executable stack:

> The use of trampolines requires an executable stack, which is a security risk. To avoid this problem, GCC also supports another strategy: using descriptors for nested functions. Under this model, taking the address of a nested function results in a pointer to a non-executable function descriptor object. Initializing the static chain from the descriptor is handled at indirect call sites.

So, if I understand it right, instead trampoline on executable stack, the pointer to function and data is pushed into the "descriptor", and then there is an indirect call to this. I guess better than exec stack, but still...

  • uecker 2 days ago

    They only need trampolines when they access their local environment and you take their address. Without optimization a trampoline was generated whenever an address was taken, but I recently changed this in the development version of GCC to only do this when needed, so hopefully in the next released version you will not get a trampoline for many more cases. Here, there is no address being taken anyway, so you do not get a trampoline.

    (and I hope we get a solution without trampolines for the remaining cases as well)

  • messe 2 days ago

    They only need an executable stack when they're not inlined.

    The always_inline keyword takes care of that here.

letmetweakit 2 days ago

I'm always suspicious of exotic features that could fail in surprising ways.

  • babel_ 2 days ago

    Well, each `defer` proposal for C agreed that it shouldn't be done the way Go does it, and should just be "run this at the end of lexical scope", so it'll certainly be less surprising than the alternative... and far easier to implement correctly on the compiler side... and easier to read and write than the corresponding goto cleanup some rely on instead. Honestly, I feel like it becomes about as surprising as the `i++` expression in a `for` loop, since that conceptually is also moved to the end of the loop's lexical scope, to run before the next conditional check. Of course, a better way of representing and visualising the code, even if optionally, would help show where and when these statements run, but a standard feature (especially with some of the proposed safety mechanisms around jumps and other ways it could fail in surprising ways) it would hardly seem exotic, and inversely is quite likely to expose things currently fail in surprising ways precisely because we don't have a simple `defer` feature and so wrote something much more complicated and error-prone instead.

    So, I completely understand the sentiment, but feel that `defer` is a feature that should hopefully move in the opposite direction, allowing us to rely on less exotic code and expose & resolve some of the surprising failure paths instead!

joexbayer 3 days ago

Small blog post exploring a defer implementation using GCC’s cleanup + nested functions, looking at the generated assembly and potential use cases.

pwdisswordfishz 2 days ago

I don't understand why people insist on simulating a poor substitute for RAII with a feature that is itself almost decent RAII.

> If malloc fails and returns NULL, the cleanup function will still be called, and there’s no simple way to add a guard inside free_ptr.

free(NULL) is a no-op, this is a non-issue. I don't know what's so hard about a single if statement anyway even if this were an issue.

  • ncruces 2 days ago

    > I don't understand why people insist on simulating a poor substitute for RAII with a feature that is itself almost decent RAII.

    RAII doesn't make sense without initialization.

    Are you proposing C should add constructors, or that C should make do without defer because it can't add constructors?

    • masklinn 2 days ago

      > RAII doesn't make sense without initialization.

      Rust has RAII and does not have constructors.

      • ncruces 2 days ago

        Rust mandates that every field in a user-defined type is initialized at once. How do you propose to retrofit that into C without "constructors"?

        • dwattttt 2 days ago

          C has had designated initializers since C99, if you want you can initialise every struct field at once.

          • ncruces 2 days ago

            I encourage you to read (at least) this section of this blog before making simplistic suggestions: https://thephd.dev/just-put-raii-in-c-bro-please-bro-just-on...

            How do you mandate initialization, handle copies, move objects, prevent double frees? What's RAII without any of that?

            • dwattttt 2 days ago

              You mandate it like you mandate anything else in C. You don't.

              You pick C because you want a language that doesn't require a variable to be initialised before mutably referencing it, and you write your defer statements or "destructors" defensively, expecting that a variable could be in any state when it comes time to dispose of it.

              Or if you find that unacceptable, you accept that C isn't the language you want. There's many other choices available.

              • ncruces a day ago

                There's no way to write a "destructor" defensively, if the contents of memory it is trying to "destroy" are undefined.

                Whereas it's perfectly possible to only defer a statement when you know the "object" has been properly initialized.

                That's why defer makes sense in a language like C (even Go), but RAII does not.

                • dwattttt a day ago

                  I agree with everything you've said, except the conclusion: C can't add proper safe RAII, but being "proper and safe" is not a threshold C even tries to uphold.

                  • ncruces a day ago

                    But would a destructor that runs automagically when a value goes out of scope, even if it's not properly initialized (and with zero regards for copies or moves) be in any way better than a defer that's explicitly called after it is initialized?

                    Cause, like, that's the entire thread.

                    • dwattttt a day ago

                      Nope, it wouldn't be. I'm just pointing out that it being unsafe or hard to use is no barrier at all to it being in the language.

                      • masklinn 10 hours ago

                        Hell, since RAII is an opt-in type-level infectious (in the sense that if you have an RAII member in a struct the struct is RAII) mechanism you can require that RAII structs be initialised.

                        Given the compiler would already need to know that a local is of an RAII type in order to insert the drop, move, and copy glues it’s not exactly a big requirement.

  • oreally 2 days ago

    poor?

    If I use RAII I'd need to have a struct/class and a destructor.

    If I use defer I'd just need the keyword defer and the free() code. It's a lot more lean, efficient, understandable to write out.

    And with regards to code-execution timing, defer frees me from such a burden compared to if-free.

    • Ygg2 2 days ago

      > If I use defer I'd just need the keyword defer and the free() code.

      Yeah, and not accidentally forgetting to call it. That's the big part. And before "True Scotsman will always free/close/defer!" - No, no they won't.

      Unless the compiler screams at them, or its enforced via syntax constructs, it will always slip through the cracks.

      • oreally 2 days ago

        Well I'd have to pay all the friction of writing up a new type, and in some cases the type gets cubersome. Doubly so if your codebase requires extra some friction like 1 header for each type.

        Also get over it. We got post-processor things like static analyzers, etc, and whatever AI code reminders/fixers that are coming up next. I'd prefer those over muddying up the code base.

        • Ygg2 2 days ago

          > Also get over it. We got post-processor things like static analyzers, etc, and whatever AI code reminders/fixers that are coming up next.

          Sure. But unless it's part of compiler, someone will not run it, or will run out of resources (no net or no tokens).

          Defaults matter a ton.

  • masklinn 2 days ago

    > I don't understand why people insist on simulating a poor substitute for RAII with a feature that is itself almost decent RAII.

    Because it’s nowhere near “almost decent RAII” and RAII requires a lot more machinery which makes retrofitting RAII complicated, especially in a langage like C which is both pretty conservative and not strong on types:

    - RAII is attached to types, so it’s not useful until you start massively overhauling code bases e.g. to RAII FDs or pointers in C you need to wrap each of them in bespoke types attaching ownership

    - without rust-style destructive moves (which has massive langage implications) every RAII value has to handle being dropped multiple times, which likely means you need C++-style copy/move hooks

    - RAII potentially injects code in any scope exit, which I can’t see old C heads liking much, if you add copy/move then every function call also gets involved

    - Because RAII “spreads” through wrapper types, that requires surfacing somehow to external callers

    Defer is a lot less safe and “clean” than RAII, but it’s also significantly less impactful at a language level. And while I very much prefer RAII to defer for clean-slate design, I’ve absolutely come around to the idea that it’s not just undesirable but infeasible to retrofit into C (without creating an entirely new language à la C++, you might not need C++ itself but you would need a lot of changes to C’s semantics and culture both for RAII to be feasible).

    https://thephd.dev/just-put-raii-in-c-bro-please-bro-just-on... has even more, mostly from the POV of backporting C++ so some items have Rust counterpoints… with the issue that they tend to require semantics changes matching Rust which is also infeasible.

  • 1718627440 2 days ago

    Not having RAII is precisely the reason I prefer C over C++ or Rust. I WANT to be able to separate allocation from initialization.

    I'm currently working with Arduino code and the API is a mess. Everything has a second set of manual constructor/destructor, which bypasses type-safety entirely. All only to shoehorn having existing, but uninitialized objects into C++.

    • mkj 2 days ago

      With Rust there are ways to do that on embedded (no heap). A wrapper StaticCell holds the allocation, then when you are ready you intialise it with the inner value. From then on work with a mut reference to the inner value. Being a bit verbose is the main downside AFAIK.

      https://github.com/embassy-rs/static-cell

    • mrheosuper 2 days ago

      >Arduino code

      Usually Arduino code is written by hobbyist that give zero care about "clean and abstraction".

      • 1718627440 a day ago

        Yes and that is another large part of work for me, but this pattern is mandated by the Arduino API themself and I don't see another way given the specification and the design of C++, short of switching over to C.

    • pif 2 days ago

      > I WANT to be able to separate allocation from initialization.

      Which hardly ever makes sense, and is possible with clean C++ anyway...

      • 1718627440 2 days ago

        > is possible with clean C++ anyway...

        That's news to me; how?

        • cozzyd 2 days ago

          Placement new?

          • 1718627440 2 days ago

            I might also use placement new after taking a pointer to a stack object or global, but it won't prevent the original constructor from being run.

    • 3836293648 2 days ago

      Both C++ and Rust allow that? Having niche behaviour not be the default makes sense, but both know it's needed and therefore allow it?

      (C++ lets you malloc and then placement new (just casting the pointer like C does is UB, but it's being fixed for trivial types) and Rust has both plain alloc and Box<MaybeUninit<T>>)

      There are a lot of other reasons not to use them, but yours is a made up strawman.

      • tomck 2 days ago

        This isn't what people are talking about, you aren't understanding the problem

        With RAII you need to leave everything in an initialized state unless you are being very very careful - which is why MaybeUninit is always surrounded by unsafe

            {
                Foo f;
            }
        
        
        f must be initialized here, it cannot be left uninitialized

            std::vector<T> my_vector(10000);
        
        EVERY element in my_vector must be initialized here, they cannot be left uninitialized, there is no workaround

        Even if I just want a std::vector<uint8_t> to use as a buffer, I can't - I need to manually malloc with `(uint8_t)malloc(sizeof(uint8_t)*10000)` and fill that

        So what if the API I'm providing needs a std::vector? well, I guess i'm eating the cost of initializing 10000 objects, pull them into cache + thrash them out just to do it all again when I memcpy into it

        This is just one example of many

        another one:

        with raii you need copy construction, operator=, move construction, move operator=. If you have a generic T, then using `=` on T might allocate a huge amount of memory, free a huge amount of memory, or none of the above. in c++ it could execute arbitrary code

        If you haven't actually used a language without RAII for an extended period of time then you just shouldn't bother commenting. RAII very clearly has its downsides, you should be able to at least reason about the tradeoffs without assuming your terrible strawman argument represents the other side of the coin accurately

      • 1718627440 2 days ago

        > malloc

        Yes, that's heap allocation. I'm talking about automatic allocation, by the compiler not getting a pointer from a library function. Like that:

            Connection connections[200];
        
        This will call the constructor, which forces me to write the class in a way that has `bool initialized`, and provide a random other method with poses as a second constructor. And now every function has to do a check, whether the constructor was called on the object or I just declare it to be UB and completely loose type safety.
  • jcupitt 2 days ago

    `free(NULL);` will crash on some platforms that gcc supports, I believe.

    • SAI_Peregrinus 2 days ago

      Then it's in violation of the C standard, at least as of C11 (I didn't check C99 or C89).

      > The free function causes the space pointed to by ptr to be deallocated, that is, made available for further allocation. If ptr is a null pointer, no action occurs. Otherwise, if the argument does not match a pointer earlier returned by a memory management function, or if the space has been deallocated by a call to free or realloc, the behavior is undefined.

      Emphasis mine

    • po1nt 2 days ago

      It shouldn't https://pubs.opengroup.org/onlinepubs/7908799/xsh/free.html

      >If ptr is a null pointer, no action occurs.

      • quietbritishjim 2 days ago

        While I agree it shouldn't, that particular document is the UNIX specification, not the C specification, so it does not apply to C compilers on non-UNIX platforms.

        • jibal a day ago

          free(NULL) is a noop ever since C89 (I was on the standards committee, X3J11).

    • lelanthran 2 days ago

      > `free(NULL);` will crash on some platforms that gcc supports, I believe.

      I'm pretty certain that `free(NULL)` is part of the C99 standard, so compiler vendors have had 25 years to address it.

      If your `free(NULL)` is crashing on a certain platform, you probably have bigger problems, starting with "Compiler that hasn't been updated in 25 years".

      • jibal a day ago

        It's in C89 (I was on the standards committee, X3J11).

    • jibal a day ago

      > `free(NULL);` will crash on some platforms that gcc supports, I believe.

      No, of course it won't. `free(NULL)` has been a noop ever since C89 (and before, for that matter).

    • unwind 2 days ago

      That feels like a "citation needed", since that would be very clear violation of the C spec and thus a rather serious bug in the standard library for that platform.

    • mrheosuper 2 days ago

      can we just do `if(*ptr == NULL) return;` ?

      • inkyoto 2 days ago

        If «ptr» is not a valid pointer, an attempt to dereference it (i.e. *ptr) will most assuredly crash the process with a SIGSEGV.

        • knorker 2 days ago

          But when would it not be a valid pointer, and yet also not a null pointer? A null pointer we can check for easily.

          • inkyoto 2 days ago

            A null pointer is not a valid pointer in a predominant number of systems in existence. If malloc (3) has returned a NULL, *ptr will cause a SIGSEGV.

            Embedded systems are an exception, though. They may not have a MMU, and in such a case the operation will succeed.

            • knorker 2 days ago

              1. No, dereferencing a null pointer will not "cause a sigsegv". It causes UB. In practice, in unix user space, yes it'll probably be SIGSEGV. 2. A null pointer is not a valid pointer: Yeah… Once again my question was "But when would it not be a valid pointer, and yet also not a null pointer? A null pointer we can check for easily."

              This code will NEVER deference a null pointer. Not under any compiler, not with any compiler options:

                  if (ptr != NULL) { *ptr = 0; }
              
              > A null pointer is not a valid pointer in a predominant number of systems in existence.

              No, that's not quite pedantically accurate. A null pointer is not a valid pointer in the C programming language. Address zero may or may not be, that's outside the scope of the C language. Which is why embedded and kernel work sometimes has to be very careful here.

              > They may not have a MMU, and in such a case the operation will succeed.

              Lack of MMU does not mean address zero is valid. It definitely* doesn't make a null pointer valid. In fact, a null pointer may not point to address zero.

              • inkyoto 2 days ago

                A zero (0, not NULL!) pointer is a valid pointer in C/C++. It is not a UB, and it means one simple thing: «give me the contents of a memory cell (a byte, a word, a long word etc) at the address of 0». Old hardware designs used the address of 0 to store a jump address of the system boot-up sequence (i.e. firmware), and I personally wrote the code in C to inspect / use it in the unpriviledged hardware mode.

                The prevailing number of modern systems do not map the very first virtual (the emphasis is on virtual) memory page (the one that starts from zero) into the process address space for pragmatic reasons – an attempt to dereference a zero pointer is most assuredly a defect in the application. Therefore, an attempt to dereference a zero pointer always results in a page fault due to the zeroeth memory page not being present in the process' address space, which is always a SIGSEGV in a UNIX.

                Embdedded systems that do not have a MMU will allow *ptr where «ptr» is zero to proceed happily. Some (not all) systems may even have a system specific or a device register mapped at the address being 0.

                You are conflating several unrelated things, and there is no pedantry involved – it is a very simple matter with nothing else to debate.

                • knorker 2 days ago

                  > it means one simple thing: «give me the contents of a memory cell (a byte, a word, a long word etc) at the address of 0»

                  Well… sometimes. If you set a pointer to literal 0, you do not actually make that pointer point to address zero, from the C language's point of view. No, you are then setting it to be the null pointer. (c99 6.3.2.3 paragraph 3)

                  Now, what is the bit value of a null pointer? That's undefined.

                  So how do you even set a pointer to point to address zero? In the C standard, maybe if you set an intptr_t to 0 and then cast it to the pointer? Actually I don't know how null pointer interacts with intptr_t 0. Is intptr_t even guaranteed to contain the same bit pattern? I don't see it. All I see is that it's guaranteed to convert back and forth without loss. For all I can find in the spec, converting between intptr_t and pointer inverts the bits.

                  A null pointer "is guaranteed to compare unequal to a pointer to any object or function".

                  Did you put an object or function at address zero? Sounds pretty UB to me.

                  > modern systems […] SEGV

                  I already agreed with you on this. I mean… now modern systems don't let applications map address zero (actually, is that always true? I know OpenBSD stopped allowing it after some security holes. I'm too lazy to check if Linux did too)

                  More info at https://stackoverflow.com/questions/63790813/allocating-addr...

                  In any case, this is a fix that's only like 10 years old (or I'm old and it's actually 20). It used to be possible.

                  > Embdedded systems that do not have a MMU will allow *ptr where «ptr» is zero to proceed happily.

                  This is absolutely not true. An embedded system could have I/O mapped to address zero reboot the machine on read or write. And that'd be perfectly fine for the C language spec, since C doesn't allow dereferencing a null pointer.

                  MMU is not the only way memory becomes magic. In fact, it's probably the LEAST of the magic memory mapping that can happen.

                  > with nothing else to debate.

                  I mean… you're just wrong. I'm not conflating unrelated things. I'm correcting multiple unrelated mis-statements you made.

                  To add the things up though: Let's say you intend to read from address zero, so you do `char* ptr = 0; something(*ptr);`. C standard would allow this to set ptr to 0xffff, and reading from that address starts the motor. The C standard doesn't say. It just says that assigning 0 sets it to null pointer, which on some systems is 0xffff.

                  I've certainly worked on embedded stuff that "did stuff" when an address was read. Sometimes because nobody hooked up the R/W pin, because why would they if the address goes to a motor where "read" doesn't mean anything anyway?

                  • inkyoto 2 days ago

                    You are conflating tha language capability with the hardware capability. C/C++ do not place restrictions on dereferencing the 0th address. Consider the following stub:

                      /* -O2  -std=c23 -Wall -fno-inline-functions */
                      int *ptr0 = 0;
                      int *ptr0p = (int *)0;
                    
                      int
                      main ()
                      {
                          return *ptr0 | *ptr0p;
                      }
                    
                    Head over to godbolt, compile it, and check the code. Zero compilation warnings, and the compiler duly obliges to generate the code that accesses a memory cell at the address 0x0 and all architectures that godbolt supports (ARM, RISC-V, SPARC64, POWER64, TI, S390 and others – with no exceptions).

                    So if you run that code on a system before the MMU is activated or on a system without a MMU, «main» will return 0 on all systems[0] (if the memory is initialised with zeroes). You do have a point that some embedded systems[1] may have device registers mapped at 0, but that bears no relevance on the generated code – it will still attempt to read the 0th address.

                    You can also test the generated code in QEMU on an architecture of your choice in the «bare metal mode» (i.e. memory protection off) and observe that a read from 0 will give you 0 if the first memory page is filled with 0s.

                    > More info at https://stackoverflow.com/questions/63790813/allocating-addr...

                    You are most assuredly conflating a pointer to 0 dereferencing with the memory protection/virtual memory management system, and the explanation is in the first answer. It is Linux that implements a kernel-level check in mmap(2) on the address to mmap into, not the hardware. It is a Linux-specific quirk, and other UNIXes will allow the mmap to 0 to proceed but reading from 0 will still yield a SIGSEGV due to memory protection being in use.

                    > MMU is not the only way memory becomes magic. In fact, it's probably the LEAST of the magic memory mapping that can happen.

                    MMU is not magic. It is a simple and very efficient design that works in concert with the microarchitecture it has been implemented for – CPU traps, memory page descriptors and tables.

                    > I mean… you're just wrong. I'm not conflating unrelated things. I'm correcting multiple unrelated mis-statements you made.

                    Respectfully, so far I am yet to see a single compelling argument or tangible piece of evidence to support the claims you have espoused. I have provided a few very concrete and specific examples as supporting evidence, but I am not seeing the same on your side.

                    [0] The only exception that does not initialiase memory with zeroes that I am aware of is AIX (but not POWER/PowerPC that it runs on!) – the AIX VMM initialises a new memory page upon allocation with 0xdeadbeef to make unintialised pointers forcefully crash the process. Linux, *BSD's running on POWER/PowerPC do not do it, it is an AIX specific quirk.

                    [1] Again, embedded may have a nuance (subject to a specific hardware* implementation) as it is a commonplace in embedded systems to not* have a contiguous memory space and have holes in it, including the zeroeth address. It does not preclude the generated code to attempt to access 0, though, if the hardware supports it.

                    • knorker 14 minutes ago

                      Oh, it's too late to edit my comment, but one more thing:

                      > You are conflating tha language capability with the hardware capability

                      No. I'm talking about the C language and the abstract machine that it defines. I'm very much NOT talking about specific hardware, unlike when you bring up MMU and other irrelevant specific hardware capability.

                      Because specific hardware capability is not part of the language.

                    • knorker 21 hours ago

                      > C/C++ do not place restrictions on dereferencing the 0th address.

                      It doesn't. Because it doesn't consider 0th address special. Null pointer though, is special. And it's not necessarily address zero.

                      > Head over to godbolt, compile it, and check the code.

                      I know what it compiles to on godbolt supported architectures. This is completely irrelevant.

                      Your code example, and its reasoning, contain so many errors that it's hard to know where to start.

                      You seem to insist that null pointer points to address zero. And you can try every architecture on godbolt, and it very likely does. But now you're just describing what happens when you do it, not the language and what can happen in the future.

                      Read this about how strange literal 0 and NULL is: https://c-faq.com/null/ptrtest.html

                      > So if you run that code on a system before the MMU is activated or on a system without a MMU, «main» will return 0 on all systems

                      "All systems"? What does that even mean? If you compile and run this on DOS you read from DS:0000 (or is it ES:0000?), which is not physical address zero, even though the MMU is not activated.

                      If you're using the Large or Huge memory model (https://en.wikipedia.org/wiki/X86_memory_models), then you may think that you'll read absolute value zero. Maybe in practice you do. But there's not necessarily a zero there. It'll be the first entry of the IVT.

                      But since you created null pointers (not "pointers to address zero"), and deferencing null pointers is UB, anything can happen. Including the compiler removing the code or just hard coding setting the result to zero.

                      But yeah, I'm not at all surprised that your misguided test gave you zero.

                      > Zero compilation warnings,

                      What's that supposed to signify? UB doesn't in any way what so ever imply warnings. -Wall doesn't even turn on all warnings anyway.

                      You can take ANY UB and "prove to me" what will happen by showing it on godbolt. But that just means you don't understand what UB even means.

                      > a read from 0 will give you 0 if the first memory page is filled with 0s.

                      This is also "not even wrong".

                      > You are most assuredly conflating a pointer to 0 dereferencing with the memory protection/virtual memory management system

                      Not even remotely.

                      > MMU is not magic.

                      You cannot think that I meant magic. Do you not understand what I meant? I meant you don't need an MMU to experience something other than plain naive address space mapped to physical RAM.

                      I've written a toy OS kernel with virtual memory, I know exactly what an MMU does.

                      > Respectfully, so far I am yet to see a single compelling argument or tangible piece of evidence to support the claims you have espoused.

                      So, everything you have said is wrong. A null pointer is not defined to point to address zero (see link above). So your code is nonsense. How am I supposed to provide some counter to that? Aside from where I have already said this, and it's plainly written in the C standard. (or, as it were, left undefined and thoroughly documented internet wide)

                      Dereferencing a null pointer is invalid. The spec says so:

                      > Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer[…]

                      c99 6.5.3.3 footnote 83.

                      You cannot dereference a null pointer according to the C language. And setting a pointer to literal 0 does not set any defined value of the pointer, other than setting it to the C "null pointer". Which could be any value. (again, see link above)

                      I "claim" that godbolt showing current compilers on some architectures do something specific is completely irrelevant to a discussion on UB. It could do the "common sense thing" (and almost always does), but it can do anything. You should read up on https://en.wikipedia.org/wiki/Undefined_behavior

                      When you say that you have not seen a single compelling argument, I think you're being hyperbolic. You said a system without an MMU will allow dereferencing a pointer to address zero (ignoring for now what that even is, in C). I replied that it's perfectly valid to have reads to address zero reset the machine.

                      What is your claim, that "no, the C standard does not allow resetting the machine when you read from address zero"? Even if a pointer to address zero and null pointer are the same (not defined by C), then per reference above it's invalid to dereference it.

                      > The only exception that does not initialiase memory with zeroes that I am aware of is AIX

                      What is this in regards to? It's seems extremely random. You are enumerating architectures whose bootup state have a certain initialized memory layout?

                      Why are you doing this? What point are you trying to prove?

                      I mean, it's also wrong. As we know from cold boot attacks, memory isn't just zeroed.

                      And when an x86 CPU starts up in real mode, address 0 has the interrupt vector table at address zero. So that's where interrupt 0 (divide error) handler far pointer goes. "Initialized to zero" doesn't make any sense. Entire books have been written about the x86 bootup process. Probably at some stages during an x86 boot address zero (virtual and/or physical) contain zero values, but it's not the long term contents.

                      Is an architecture that maps an I/O device for a temperature sensor at address zero an invalid architecture? Is it fundamentally incompatible with C? Of course not. Modern unix provides a user space where reading at address zero causes SEGV. But that's just one nuanced environment out of many.

                      > Again, embedded may have a nuance (subject to a specific hardware implementation) as it is a commonplace in embedded systems to not have a contiguous memory space and have holes in it

                      Vanilla unix user space is also noncontiguous with holes in it. Including usually (nowadays) a hole at address zero.

                      This is all completely unrelated to C language null pointers. Completely. You say I'm conflating things, but you're conflating null pointers with pointers to address zero. And with if address zero is initialized. And with MMU. None of which are relevant to any of the other things.

                      > Again, embedded may have a nuance

                      Everything has nuance. That's the entire point of the C standard. If you code to the C standard abstract machine, the compiler promises a certain observed behavior when run on the target. If you go beyond the standard (such as assuming nullptr points to address zero), then you are making assumptions about the nuances of the specific machine AND the compiler.

                      Linux has nuance. OpenBSD has nuance. Every embedded platform definitely has nuance. GCC has nuance (e.g. you can disable null check elision optimization. Is that true of MSVC? (I haven't used MSVC since last millenium).

                      But sure, if you say "my software only supports architectures where nullptr is zero, and built on a compiler that doesn't use this optimization", then that's your choice. It may be hard to enforce that over the lifetime of the program though, so maybe best to just write C code without UB. (which, as any expert will tell you, is nontrivial) Extra hard as future compilers or compiler versions introduce new ways of turning UB into "surprises".

                      Anyway, I'm done. Like I said, your theories about null pointers, UB, and architectures are intertwined in a web of misunderstanding that would take more than a HN thread to untangle.

        • mrheosuper 2 days ago

          The "ptr" is a pointer to pointer, not just a pointer, you are not dereferencing Null ptr, so i expect nothing to crash.

      • menaerus 2 days ago

        No, because optimizing compilers are free to elide the check. https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#ind...

        • mrheosuper 2 days ago

          I'm not quite familar with this flag, but this

          >so that if a pointer is checked after it has already been dereferenced, it cannot be null.

          sound to me that if i've never deref the pointer anytime before(e.g the null check is at the beginning of function), the compiler won't remove this check.

          • menaerus 2 days ago

            Since the compiler will merge/fold what it appears to be a different logic sections of your code into a single one, you can never be sure what the release build codegen looks like unless you read the assembly.

            • knorker 2 days ago

              If you check for null pointer before you dereference, then no the compiler cannot elide the check.

              If you check after dereferencing it, yes it can. But in this case why would you not check before dereferencing? It's the only UB-free choice.

              • menaerus 2 days ago

                Yes, it can. Why would you be checking the pointer for nullptr after you have dereferenced it? It makes no sense at all, so, compiler indeed can elide the nullptr check before dereferencing the ptr exactly because it is free to _always_ assume that the program is free of UB.

                To be more precise GCC says "eliminate useless checks for null pointers" and what I am saying that you can never be sure what in your code ended up being "useless check" vs "useful check" according to the GCC dataflow analysis.

                Linux kernel is a famous example for disabling this code transformation because it is considered harmful. And there's nothing harmful with the nullptr check from your example.

                • knorker 2 days ago

                  > Why would you be checking the pointer for nullptr after you have dereferenced it? It makes no sense at all

                  Right. It's UB. And that's why the optimization in question is about removing that check. The only reason the optimization is valid for a C compiler to do, is that it can assume dereferencing a null pointer lands you in UB land.

                  I'm sorry, either you are terrible at trying to explain things, or you have thoroughly misunderstood what all this is about. GCC cannot, under any circumstances or with any flags, remove an "if (ptr == NULL)" that happens before dereferencing the pointer.

                  What this flag is about, and what the kernel bug you mentioned (at least I think you're referring to this one) is about, was a bug that went "int foo = ptr->some_field; […] if (ptr == NULL) { return -EINVAL; }". And GCC removed the post-deref null pointer check, thus making the bug exploitable.

                  From the help text:

                  > if a pointer is checked after it has already been dereferenced, it cannot be null.

                  after. Only applies after. A check before dereferencing can never be removed by the compiler.

                  Obviously.

                  • 1718627440 2 days ago

                    I think where you are talking past each other is, that one is talking about temporal after and the other about causal after. The null check can be eliminated if the dereference happens temporally after, but causally before:

                        if (ptr == NULL)
                        {
                            ...
                        }
                    
                        ...
                    
                        int foo = ptr->some_field;
                    • knorker 2 days ago

                      Ah yes, unless that conditional returns, UB indeed has time travelling properties. (I mean, spec wise. Manifesting as the compiler "reasoning" that "well I could legally put that load before the check, and that means it's not null, which means I don't need the check")

                      But this brings us back to the article: Why does the author say that there's no way to check for NULL in the free function? Maybe they are hinting at something completely unrelated to what we're saying here?

                      If that's where we failed to communicate, then that makes sense. Thanks, stranger!

                • lelanthran 2 days ago

                  > Yes, it can.

                  I don't think so. If it could, then this code would reliably crash:

                      char *mystr = strdup (oldstr);
                      if (mystr)
                          *mystr = 0; // Truncate string
                  
                  That never crashes.
        • chongli 2 days ago

          Not on all platforms! If you’re writing portable code targeting a lot of embedded platforms then you don’t want to rely on this optimization.

          • menaerus 2 days ago

            It's a platform-agnostic optimization in case of GCC so if your embedded Linux toolchain is based on GCC, and most of them are, it's pretty much the case that it will have this optimization turned on by default.

            > This option is enabled by default on most targets. On AVR and MSP430, this option is completely disabled.

            • chongli 2 days ago

              Yes and if you’re targeting AVR, an extremely popular 8 bit micro, then it’ll be turned off.

        • jibal a day ago

          Yes, but `*ptr == NULL` is just plain wrong ... it should be `ptr == NULL` ... but that test is redundant since `free` is required to do it.

      • jibal a day ago

        > can we just do `if(*ptr == NULL) return;` ?

        No, certainly not, but you can do

        `if(ptr == NULL) return;`

        which is correct but unnecessary since `free` is required to do that check.

grodes 2 days ago

Seems good, but I do not care about cleanup memory since I started to use arenas.

  • accelbred 2 days ago

    This is still useful for cleaning up file descriptors, unlocking mutexes, etc.

  • jcupitt 2 days ago

    Cleanup can be very useful if you depend on a library that does not support arenas.

mgaunard 2 days ago

Just use C++, it's its main feature on top of C.

  • lelanthran 2 days ago

    > Just use C++, it's its main feature on top of C.

    If you want to and/or can, then go ahead. This is for those people who either don't want to, or can't, use C++.

    Are you suggesting only use C++ over C in all situations?

    • mgaunard 2 days ago

      Yes.

      • lelanthran a day ago

        >> Are you suggesting only use C++ over C in all situations?

        > Yes.

        You cannot imagine any situation where that proposal is a non-starter? Or a deal-breaker?

        • mgaunard a day ago

          Not in most places. Even GCC moved to build their C code as C++.

          • lelanthran a day ago

            > Not in most places.

            So you can imagine that C++ instead of C is a deal-breaker or show-stopper in some places, right?

            Now that we both agree that there are some communities who won't move off C, do you believe that those communities should never get any upgrade, security fixes, etc?

  • babel_ 2 days ago

    > on top of C.

    If we're referring to the "C is a subset of C++" / "C++ is a superset of C" idea, then this just hasn't been the case for some time now, and the two continue to diverge. It came up recently, so I'll link to a previous comment on it (https://news.ycombinator.com/item?id=45268696). I did reply to that with a few of the other current/future ways C is proposing/going to diverge even further from C++, since it's increasingly relevant to the discussion about what C2y (and beyond) will do, and how C code and C++ code will become ever more incompatible - at least at the syntactic level, presuming the C ABI contains to preserve its stability and the working groups remain cordial, as they have done, then the future is more "C & C++" rather than "C / C++", with the two still walking side-by-side... but clearly taking different steps.

    If we're just talking about features C++ has that C doesn't, well, sure. RAII is the big one underpinning a lot of other C++ stuff. But C++ still can't be used in many places that C is, and part of why is baggage that features like RAII require (particularly function overloading and name mangling, even just for destructors alone)... which was carefully considered by the `defer` proposals, such as in N3488 (recently revised to N3687[0]) under section 4, or in other write-ups (including those by that proposal's author) like "Why Not Just Do Simple C++ RAII in C?"[1] and under the "But… What About C++?" section in [2]). In [0] they even directly point to "The Ideal World" (section 4.3) where both `defer` and RAII are available, since as they explain in 4.2, there are benefits to `defer` that RAII misses, and generally both have their uses that the other does not cleanly (if at all) represent! Of course, C++ does still have plenty of nice features that are sorely missing in C (personally longing for the day C gets proper namespaces), so I'm happy we always have it as an option and alternative... but, in turn, I feel the same about C. Sadly isn't as simple to "just use C++" in several domains I care about, let alone dealing with the "what dialect of C++" problem; exceptions or not, etc, etc...

    [0]: https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3687.htm [1]: https://thephd.dev/just-put-raii-in-c-bro-please-bro-just-on... [2]: https://thephd.dev/c2y-the-defer-technical-specification-its...

    • captainmuon 2 days ago

      > this just hasn't been the case for some time now

      Which I find sad actually. The idea of C++ as a superset of C is really powerful, especially when mixing C and C++. A while ago I had a C project (firmware for a microcontroller) and wanted to bake the version and the compilation time into the firmware. I didn't find a way to do this in plain C, but in C++ you can initialize a global struct and it gets statically linked into the output. This didn't even use constexpr, just preprocessor trickery. Then it was just a matter of renaming the c file to cpp and recompiling. I guess you could also do that with C, but there are things like RAII or constexpr or consuming a C++ library that you can't do without.

      • lelanthran 2 days ago

        > wanted to bake the version and the compilation time into the firmware. I didn't find a way to do this in plain C, but in C++ you can initialize a global struct and it gets statically linked into the output. This didn't even use constexpr, just preprocessor trickery.

        I might be misunderstanding here, but if you are okay with preprocessor trickery, then it's doable.

        I do this routinely in the Makefile, which (very tediously) generates a build_info module (header and implementation) that is linked into the final binary: https://github.com/lelanthran/skeleton-c/blob/8e04bed2654dac...

      • 1718627440 2 days ago

        > I didn't find a way to do this in plain C

        Not sure what you were running into. I routinely do this just fine.

        > This didn't even use constexpr, just preprocessor trickery.

        Isn't the preprocessor shared between C and C++?

        > in C++ you can initialize a global struct and it gets statically linked into the output

        That sounds to be doable just the same in C?

    • pjmlp 2 days ago

      > But C++ still can't be used in many places that C is

      Unless we are speaking about PICs or similar old school 8 and 16 bit CPUs, with compilers like those from MIKROE, there is hardly a platform left were the vendor compiler isn't C and C++ (even if it doesn't go beyond C++11).

      And if it must be deployed as freestanding, there are still enough improvements to take advantage of.

      In the end it boils down to human factor in most cases, however as Dan Saks puts "If you're arguing, you're losing.", taken from

      CppCon 2016: “extern c: Talking to C Programmers about C++”

      https://www.youtube.com/watch?v=D7Sd8A6_fYU

    • 1718627440 2 days ago

      > personally longing for the day C gets proper namespaces

      I think in the spirit of C, this should go into the linker, not in the compiler.

      • 1718627440 20 hours ago

        Or you can use objcopy --prefix-symbols=Namespace .

    • mgaunard 2 days ago

      among the people that refuse C++ and stick to C, very few are willing to look at C23.