Hacker News new | ask | show | jobs
by pwdisswordfishz 266 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.

5 comments

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

> RAII doesn't make sense without initialization.

Rust has RAII and does not have constructors.

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"?
C has had designated initializers since C99, if you want you can initialise every struct field at once.
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?

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.

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.

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

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.

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

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

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

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

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

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

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

That's news to me; how?

Placement new?
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.
>Arduino code

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

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

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

> 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.
`free(NULL);` will crash on some platforms that gcc supports, I believe.
> `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".

It's in C89 (I was on the standards committee, X3J11).
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

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

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

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.
free(NULL) is a noop ever since C89 (I was on the standards committee, X3J11).
> `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).

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.
can we just do `if(*ptr == NULL) return;` ?
If «ptr» is not a valid pointer, an attempt to dereference it (i.e. *ptr) will most assuredly crash the process with a SIGSEGV.
But when would it not be a valid pointer, and yet also not a null pointer? A null pointer we can check for easily.
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.

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.

The "ptr" is a pointer to pointer, not just a pointer, you are not dereferencing Null ptr, so i expect nothing to crash.
No, because optimizing compilers are free to elide the check. https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#ind...
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.

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

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

Yes and if you’re targeting AVR, an extremely popular 8 bit micro, then it’ll be turned off.
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.
> 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.