Readit News logoReadit News
syockit · 3 months ago
Check against FLT_EPSILON. Oh boy.

The reason is floating point precision errors, sure, but that check is not going to solve the problems.

Took a difference of two numbers with large exponents, where the result should be algebraically zero but isn't quite numerically? Then this check fails to catch it. Took another difference of two numbers with very small exponents, where the result is not actually algebraically zero? This check says it's zero.

syncsynchalt · 3 months ago
Yeah, at the least you'll need an understanding of ULPs[0] before you can write code that's safe in this way. And understanding ULPs means understanding that no single constant is going to be applicable across the FLT or DBL range.

[0] https://en.wikipedia.org/wiki/Unit_in_the_last_place

breckinloggins · 3 months ago
Other resources I like:

- Eskil Steenberg’s “How I program C” (https://youtu.be/443UNeGrFoM). Long and definitely a bit controversial in parts, but I find myself agreeing with most of it.

- CoreFoundation’s create rule (https://stackoverflow.com/questions/5718415/corefoundation-o...). I’m definitely biased but I strongly prefer this to OP’s “you declare it you free it” rule.

quelsolaar · 3 months ago
Thanks for the shout out. I had no idea my 2h video, without a camera 8 years ago would have such legs! I should make a new one and include why zero initialization is bad.
elcapitan · 3 months ago
Thank you for recording it! :) It hits the right balance between opinionated choices with explanations and a general introduction to "post-beginner" problems which probably a lot of people who have programming experience, but not in C, face.
writebetterc · 3 months ago
I can't edit my comment any longer, but I really like nullprogram.com
capyba · 3 months ago
Same here! That’s a great blog with a lot of good advice.
writebetterc · 3 months ago
void* is basically used for ad-hoc polymorphism in C, and it is a vital part of C programming.

    void new_thread(void (*run)(void*), void* context);
^- This let's us pass arbitrary starting data to a new thread.

I don't know whether this counts as "very few use cases".

The Memory Ownership advice is maybe good, but why are you allocating in the copy routine if the caller is responsible for freeing it, anyway? This dependency on the global allocator creates an unnecessarily inflexible program design. I also don't get how the caller is supposed to know how to free the memory. What if the data structure is more complex, such as a binary tree?

It's preferable to have the caller allocate the memory.

    void insert(BinTree *tree, int key, BinTreeNode *node);
^- this is preferable to the variant where it takes the value as the third parameter. Of course, an intrusive variant is probably the best.

If you need to allocate for your own needs, then allow the user to pass in an allocator pointer (I guessed on function pointer syntax):

    struct allocator { void* (*new)(size_t size, size_t alignment); void (*free)(void* p, size_t size); void* context; }.*

mrkeen · 3 months ago
void* is a problem because the caller and callee need to coordinate across the encapsulation boundary, thus breaking it. (Internally it would be fine to use - the author could carefully check that qsort casts to the right type inside the .c file)

> What if the data structure is more complex, such as a binary tree?

I think that's what the author was going with by exposing opaque structs with _new() and _free() methods.

But yeah, his good and bad versions of strclone look more or less the same to me.

warmwaffles · 3 months ago
Curious about the allocator, why pass a size when freeing?
naasking · 3 months ago
If you don't pass the size, the allocation subsystem has to track the size somehow, typically by either storing the size in a header or partitioning space into fixed-size buckets and doing address arithmetic. This makes the runtime more complex, and often requires more runtime storage space.

If your API instead accepts a size parameter, you can ignore it and still use these approaches, but it also opens up other possibilities that require less complexity and runtime space by relying on the client to provide this information.

Deleted Comment

zoomablemind · 3 months ago
"...C is my favorite language and I love the freedom and exploration it allows me. I also love that it is so close to Assembly and I love writing assembly for much of the same reasons!"

I wonder what is author's view about user's reasons to choose a C API?

What I mean is users may want exactly the same freedom and immediacy of C that the author embraces. However, the very approach to encapsulation by hiding the layout of the memory, the use of accessor functions limits the user's freedom and robs them of performance too.

In my view, the choice of using C in projects comes with certain responsibilities and expectations from the user. Thus higher degree of trust to the API user is due.

f1shy · 3 months ago
> Make sure that you turn on warnings as errors

I’m seeing this way too often. It is a good idea to never ignore a warning, an developers without discipline may need it. But for god’s sake, there is a reason why there are warnings and errors ,and they are treated differently. I don’t think compiler writers and/or C standards will deprecate warnings and make them errors anytime soon, and for good reason. So IMHO is better to treat errors as errors and warnings as warnings. I have seen plenty of times this flag is mandatory, and to avoid the warning (error) the code is decorated with compiler pacifiers, which makes no sense!

So for some setups I understand the value, but doing it all the time shows some kind of lazyness.

Chabsff · 3 months ago
> and to avoid the warning (error) the code is decorated with compiler pacifiers, which makes no sense!

How is that a bad thing, exactly?

Think of it this way: The pacifiers don't just prevent the warnings. They embed the warnings within the code itself in a way where they are acknowledged by the developer.

Sure, just throwing in compiler pacifiers willy-nilly to squelch the warnings is terrible.

However, making developers explicitly write in the code "Yes, this block of code triggers a warning, and yes it's what I want to do because xyz" seems not only perfectly fine, but straight up desirable. Preventing them from pushing the code to the repo before doing so by enabling warnings-as-errors is a great way to get that done.

The only place where I've seen warnings-as-errors become a huge pain is when dealing with multiple platforms and multiple compilers that have different settings. This was a big issue in Gen7 game dev because getting the PS3's gcc, the Wii's CodeWarrior and the XBox360's MSVC to align on warnings was like herding cats, and not every dev had every devkit for obvious reason. And even then, warnings as errors was still very much worth it in the long run.

f1shy · 3 months ago
IMHO readability is the absolute maximum paramount priority. Having the code interrupted by pacifiers makes the code more difficult to read. The warning is very visible when compiling. Let me argue, much more visible. Why? well, independent if my last change had something directly to do with that piece of code, I will see the warning. If I use some preprocessor magic, I will only see that if I directly work in that part of the code.

Again, IMHO the big problem is people think "warnings are ok, just warnings, can be ignored".

And just as anecdotal point "Sure, just throwing in compiler pacifiers willy-nilly to squelch the warnings is terrible." this is exactly what I have seen in real life, 100% of the time.

pizlonator · 3 months ago
Good stuff.

Only things I disagree with:

- The out-parameter of strclone. How annoying! I don't think this adds information. Just return a pointer, man. (And instead of defending against the possibility that someone is doing some weird string pooling, how about jut disallow that - malloc and free are your friends.)

- Avoiding void. As mentioned in another comment, it's useful for polymorphism. You can do quite nice polymorphic code in C and then you end up using void a lot.

syncsynchalt · 3 months ago
Yes that section raised my hackles too, to the point where I'm suspicious of the whole article.

The solution, in my opinion, is to either document that strclone()'s return should be free()'d, or alternately add a strfree() declaration to the header (which might just be `#define strfree(x) free(x)`).

Adding a `char **out` arg does not, in my opinion, document that the pointer should be free()'d.

masfoobar · 3 months ago
My only gripe is with Vec3_new() function in "Memory Ownership" section.

It assumes you want a single malloc of Vec3. It tries to behave as if you are doing a 'new' in an OOP language.

Let the programmer decide the size of it.

Mock example (not tested)

  struct Vec3* Vec3_new(size_t size)
  {
    if(size <= 0) {
      // todo: handle properly
      return NULL;
    }
  
    struct Vec3 *v = malloc(sizeof(struct Vec3) * size);
  
    size_t i;
    for(i = 0; i < size; i++) {
      v[i].x = 0.0F;
      v[i].y = 0.0F;
      v[i].z = 0.0F;
    }
  
    return v;
  }

1718627440 · 3 months ago
> In making code readable, you should only use char* or unsigned char* for strings (character arrays). If you want a block of bytes/memory pointer, then you should use uint8_t* where uint8_t is part of stdint.h. This makes the code much more readable where memory is represented as an unsighned 8-bit array of numbers (byte array). Now you can trust when you see a char* that it is referring to a UTF-8 (or ASCII) character array (text).

I use uint8_t for 8-bit integers, unsigned char for memory and char for text. uint8_t for memory doesn't feels right.