Hacker News new | past | comments | ask | show | jobs | submit login

* +1 to Valgrind.

* +1 to sanitizers.

* +1 to clangd and bear.

* +1 to namespacing.

* Make sure you have a code formatter. I use clang-format, but any should work.

* Do not use recent C features unless you need them. VLA's in particular are a bad choice. An example of something you might need is `alignof()` and `max_align_t`, which are basically necessary if writing your own allocator(s).

* Write destructors for your types. If they own data, their destructor should free that data. This has helped me avoid countless memory leaks.

* Finally (and this will probably be controversial), use unsigned arithmetic as much as you can! The reason for this is because unsigned arithmetic has far fewer cases of UB, and with a little work, you can simulate two's-complement signed arithmetic in C without UB.

Source: I am a C programmer with a public project that has had very few memory bugs or UB bugs found in releases. In particular, unsigned arithmetic is what got me farther than most, since most will use Valgrind/sanitizers.




> * Finally (and this will probably be controversial), use unsigned arithmetic as much as you can! The reason for this is because unsigned arithmetic has far fewer cases of UB, and with a little work, you can simulate two's-complement signed arithmetic in C without UB.

I would recommend against this. You're trading off one form of incorrect behavior (UB) for another form of incorrect behavior (wraparound on overflow is incorrect >99% of the time), with the added potential for mistakes in the emulation step.

Instead, do this:

* If you consider that wraparound on overflow is in fact the correct and desired behavior, use unsigned arithmetic.

* If you want to check for overflowing conditions, use your compiler's builtins for checked operations (C23 finally adds ckd_* ops, but it's too new to recommend use of yet).


One of the reasons I recommend unsigned arithmetic is because you can make checked arithmetic from it. Doing it with signed arithmetic is a fool's errand.

I have, in fact, done this.


It is best not to try to roll your own checked arithmetic. Your compiler already has an implementation, that will likely compile down to "check the overflow flag in hardware", and carries 0 risk of undefined behavior. Why not use it?

("It's not portable" is a reason that will cease to be true shortly--as mentioned, C23 added checked overflow functions.)


> ("It's not portable" is a reason that will cease to be true shortly--as mentioned, C23 added checked overflow functions.)

So in practice, today, it's not portable and won't be for a while (even when it's released into compilers, it'll take time for those new versions to trickle into the ecosystem), which is a compelling argument against using it.


Because I want to stay on C11.

> Your compiler already has an implementation

That's a big assumption that is not true on all compilers I want to target.


For a new project, make/bear are really not not the best any more. Something that outputs ninja would be much better. Faster, supports compile_commands.json out of the box, profiling support, etc.

One thing I'd add to the list: stdint.h. Don't use int, long, etc. Just use uint32_t, uint64_t and friends. Completely eliminates the ambiguity and you don't have to remember which is which.


> Do not use recent C features unless you need them. VLA's in particular are a bad choice.

VLA are closer to a deprecated feature than a recent feature: they were mandatory in C99, and made optional in C11.


I don't like bear. If you want to update the compilation database with it, you have to do a clean build. Most build systems can generate the database without a clean build, and most compilers can generate it, so I don't see much use in bear, personally.


how? cmake can do it, so does compiledb, but that's about it in addition to bear?

some makefile based build codebase can not build compile_commands.json at all, I had to use the old ctags way to jump around, as clangd could not understand the code(e.g. it can find header definitions, but could not find function implementations in other non-header source files)


Clang itself can do it, as can forks of Clang, so can Xmake, Meson, and Ninja, as well as Clang Power Tools for Visual Studio.


clangd can barely do it in practice, it does not index the whole code base, instead it builds up the index gradually, and it often failed to find function implementation, it can just find those definitions, ctags has no issues to find both.

I would love to stick with clangd if it can do whatever ctags can as far as source indexing goes, it seems can not.


Clangd consumes the compilation database, it doesn't produce it. Clang is what does that.


Fair enough.


Yeah, I really don't understand why people use signed integers so often. They seem... quirky. Especially in C where signed overflow is UB. I guess they're syntactically the default.

Would you consider the fixed width integer types (uint8_t, etc) a recent C feature worth using? The older type's size being implementation defined seems like a major pain to me, but I haven't used C much.


Yes, fixed-width integer sizes are absolutely worth using.


stdint.h defining the fixed width integer is part of C99. I wouldn't call that recent !

https://pubs.opengroup.org/onlinepubs/009695399/basedefs/std...


You need to understand the timescales of C.

C89 (aka ANSI C) was "standard" C for years. C99 was released, and largely ignored until the mid-2000's, and then gradually began to grow. As of about 2015, C99 was probably the "standard" C dialect, but there is still a lot of C89 out there, especially where you have niche toolchains.

I wouldn't seriously consider C11 unless you're certain that the toolchain supports it, and there's some feature that you really need. And that's pretty unlikely.


I would also recommend CodeChecker (static analysis tool - open source) or if you end up having to use gcc use gcc's -fanalyzer option for static analysis.

I find that gcc is a version or two behind clang for new compiler diagnostics and static analysis features (purely anecdotal so please take it with pinch of salt).


can you elaborate on 'namespacing' in C?

also, which way you use to do 'destructors' in C?


Namespacing was mentioned by another comment, but I do something like this:

    libname_module_function();
So for my bc [1], I do this:

    bc_parse_if(...);
That parses an `if` statement.

For destructors, I want them to be passed and stored as first-class entities, so I defined them like this:

    typedef void (*BcDestructor)(void* item);
Then things like my resizable arrays store the destructor for the items in the array, and when items are removed (popped, whatever), the destructor is run on the item if the destructor is not NULL.

Works like a charm.

Edit: There is one danger to the destructors: you can pass the wrong type of pointer. But if this happens, you'll probably get a lot of Valgrind errors and a spectacular crash.


"namespacing" in C means prefixes on public symbols.

Typically, they're two-part. First you have the project/company prefix, and then the ADT (abstract data type) prefix. So, eg. "foo_hash_" is a prefix from company/project "foo" and is a part of the "hash" ADT interface.

As a consequence, C people like abbreviations. Which is fine in moderation.

Private functions don't need the prefixes, of course.

Public variables are a little problematic if you ever port to Windows (DLLs), so try to avoid them.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: