The trouble I've run into with that is different compilers have different warnings, sometimes they are mutually contradictory, so it is not possible to have portable code that doesn't trigger warnings in one compiler or another.
I.e. the general problem with warnings is the language gets balkanized into multiple competing and incompatible dialects.
What should happen is to carefully examine the warnings, and adopt the best into the language Standard as errors.
For example:
if (a < b < c)
That is pretty much 100% a bug in the code. Just make it illegal already. D does.For another:
for (i = 0; i < 10; ++i);
{
...
}
Long ago, an expert C programmer came to me once and said he'd spent all day trying to figure out why his loop only executed once. I pointed to the semicolon. He sighed. I put a warning for that in the compiler. I noticed that other compilers eventually did, too.In D, it's an error.
A third example:
In the Joint Fighter coding standard, it says don't use `l` as an integer suffixe, as in `124l`, because in many fonts it looks like `1241`. Solution: D does not allow `l` as a suffix. There's is no reason to support that suffix. No reason to put it in coding standard. Do the world a favor, just make it illegal. Done!
• There's a built-in way to silence specific warnings for specific portions of the code. When the compiler is wrong, instead of obfuscating the code to hide it from the compiler, you can directly mark "I know what I'm doing". Standard C can't do it, and compiler-specific #pragmas are much more clunky.
• Builds of dependencies automatically suppress lints, so builds with a -Werror equivalent won't break on someone else's code (that you may not be able to fix), just because your compiler has added new lints.
Right, there are definitely some C++ specific warnings for different language compat rules (around std::move IIRC) that are non-mutually-satisfyable.
This article recommends -Weverything w/ Clang. Clang developers DO NOT ENDORSE the use of -Weverything.
On the other hand the intention (assuming numerical types) is clear, if people keep doing this, what machine code is emitted by common compilers for the "best" way to express this? And how about for the idiomatic way to express it? Perhaps the insight, if it's common, is that we should provide a nice way to do this which emits efficient machine code.
Rust says "chained comparison" is forbidden, so it knows what we're going for here, and indeed it suggests you might want (a < b) && (b < c) which is how I'd write this, but of course the opposite could be faster, as perhaps could ((a+1)..c).contains(&b) and either may make more sense in context of the usage.
The problems:
1. the C Standard Library doesn't use them
2. integer promotions rules still pertain to unsigned char, unsigned short, etc.
3. more generally, the language semantics are not defined in terms of those types
I think many C programmers might disagree with large parts of this, however. Some things that caught my eye that might be controversial:
> Wrap your structs in a typedef
The only reason to do this that's mentioned is "annoyance", which I think is a weak reason if you have the kind of problem for which you're considering to use C. The author already mentions that it has a weird interaction with forward declarations. It's my understanding that many C codebases don't do this, simply to be more explicit about what's a struct and what's not, and to avoid typedef explosion.
> the POSIX standard reserves the ‘_t’ postfix for its own typenames to prevent collisions with user types – make of that what you will ;)
IMO, that's not something to casually dismiss with a smiley. If it's reserved, don't do it.
> Typedef only creates a weak type alias not a proper new type (it’s really not much better than a preprocessor define), meaning there’s no warning when assigning to a different type from the same base type
The point is somewhat valid, but the author seems a bit confused about terminology here. It's not "a different type from the same base type", instead, it's a different name for the same type. The comparison with the preprocessor is unwarranted.
> Be (somewhat) afraid of pointers
Why? The following rant about RAII doesn't really give that many clues. Handles can have a performance penalty vs pointers -- or be a performance benefit, this really depends on the details.
Also, this is where it might have been helpful to go into different conventions about allocations, whether the caller or callee should allocate, etc.
This is why after much consideration, D uses `alias` instead of `typedef`. `alias` works for other things, too, like creating an alias for a symbol:
alias sqrt = math.std.sqrt;
x = sqrt(y);
or when you're sick of Java style names: alias eggs = CookAndEatEggsForBreakfast;
eggs();
Of course, in C you'd use a macro for this. But C macros do not respect scoping, so using them is akin to using a table saw without a face shield.How about "Java/Groovy" or "C#/VB"? C++ and C are related enought to write that I think. Especially since it is common to mix the languages in projects.
> IMO, that's not something to casually dismiss with a smiley. If it's reserved, don't do it.
It only matters when your own type names collide with any of the POSIX type names, and it's not like POSIX is changing much nowadays. A collision with 'recent' C standard additions is much more likely, and those are not predictable anyway (such as alignas() or unreachable()).
There's another blog post about memory management which might have aged a bit better (at least it's less controversial heh):
https://floooh.github.io/2018/06/17/handles-vs-pointers.html
If you have a forward declare you can then bring in different headers (or use a pre-processor block) depending on need that will redefine the function.
The reason this ability exists, in all the different incantations you can use, is to ensure that very old-style C will still compile.
You could also pop things off the stack manually inside a function all the time, even without a paramter list, popping off an "invisible" parameter also let you do things like return to a different part of the code than the invoking function.
The compiler I worked on in 1985-ish supported forward declarations, which was "fancy and new" at the time.
Insert obligatory, "I was there Gandalf, 3,000 years ago."
char *buf __attribute__ ((__cleanup__(free_buf))) = malloc(1024);
This calls free_buf(buf) when buf goes out of scope.
I’ve actually seen projects use it in the wild to close files or free memory in branchy functions. [1]
[1] https://github.com/containers/bubblewrap/blob/c54bbc6d7b78e7...
The better usage by far is describing ownership. Does a function that takes a pointer take ownership of the pointer? Who knows! It's a mystery! Does a function that takes a std::unique_ptr take ownership of the pointer? You're goddamn right it does.
Now do the same with things like file descriptors. Managing FDs in Linux is nightmare difficulty because if you get it wrong there's almost never a crash or segfault to tell you about it. Valgrind won't help you find it. Nothing helps you, you're entirely on your own. In Rust the compiler validates ownership for you, trivially made robust. In C++ you can make a "unique_fd" or similar, and at least make accidental mistakes harder. In C? Idk, apparently according to this article that's just a "many small allocations" and you're just a shitty programmer for doing that (wow, such useful advice lol)
char *buf __attribute__ ((__cleanup__(free_buf))) = malloc(1024);
D: char* buf = cast(char*)malloc(1024);
scope (exit) free(buf);
https://dlang.org/spec/statement.html#scope-guard-statementThe rationale for it:
https://dlang.org/articles/exception-safe.html
The idea for this came from Andrei Alexandrescu and Petru Marginean who proposed it for C++:
“Any sufficiently complicated C contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of C++.”
See this hack as well as macros emulating templates for generic data structures and roll your own vtables with function pointers and _Generic for function overloading and function prefixes for namespaces.
In C++, you get RAII destructor tied to the type, not the instance.
With this GNU C extension, you must remember to use it, which IMO is error prone.
Modern C for C++ Peeps (2019) - https://news.ycombinator.com/item?id=27288145 - May 2021 (21 comments)
Modern C for C++ Peeps - https://news.ycombinator.com/item?id=21093727 - Sept 2019 (16 comments)
only if you ignore things like iostreams, thread locks, etc.
I can grab a mutex with std::unique_lock and not have to worry about releasing it manually, which means simpler code and fewer bugs.
C is great for what it is, but does this article really knock down the "C is a subset of C++" argument?
Isn't this just... wrong? I mean I write in a common subset of C and C++, starting from ANSI C or C99 if required, and then when I am later required to use C++ features, I try to isolate those portions, and expose them through C interfaces.
Maybe the author meant something else, and I am misreading this.
Wall and Weverything aren't even close to all warnings. Not even a good percentage of them.
They're useful, but many other warnings exist that can be very helpful.
I thought that C99 got rid of the "implicitly allows any number of arguments" thing. Maybe I'm mistaken.
The place where I used to see this was header files that might be used by a pre-ANSI compiler. They would omit arguments in the header declarations of functions. I even remember some X11 headers putting those arguments in an ifdef, so that if you had a decent compiler you'd get the checks, but it would still work on ancient compilers.
It wasn't that the function bodies would have args missing and somehow use them. It was about declarations, the kind you see in header files.
The article doesn't mention it, but C11 allows typedef redefinitions which means instead of forward declaring "struct bla_t" and referring to it with the struct keyword, you can instead forward declare it as "typedef struct bla_t bla_t" and refer to it _without_ the struct keyword.
See N1360 [1] for details.
[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1360.htm
Things like ECMAScript style modules, structurally evaluated type aliases to produce contract types, Go-like syntax sugar for associating functions with a struct without introducing classes, compile time type parameters (generics), and only have .c files - eliminating header files.
Would prefer something that keeps with modern C++ if possible.
Here are some things I almost always do to improve ergonomics (some of this is debatable and none of this should be exposed in a library, at least prefix things)
// common.h
#pragma once
#include <assert.h>
#include <stddef.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#define ARRAY_COUNT(a) ((int) (sizeof (a) / sizeof (a)[0]))
#define STRUCT(name) typedef struct name name; struct name
#define CONTAINER_OF(ptr, type, member) ((type *) ((char *) (ptr) - offsetof(type, member)))
#define ALIGN(n) __declspec(align(n))
#define CACHE_ALIGN ALIGN(64) //XXX
typedef uint8_t u8;
typedef uint16_t u16;
//...
Each .c file includes "common.h" as the first thing.
Each .h file has #pragma once at the beginning.ARRAY_COUNT(a) is used to get the capacity of a C array without using defines (which is brittle). Unfortunately it's imperfect because code breaks when you change arrays to dynamically allocated buffers and you forget to switch to using dynamic capacity values. This is one case where I'd like to see some standards update that allows us to improve safety.
STRUCT(x) is used to declare struct x with typedef -- I've grown to hate tag namespaces for their boilerplate. I simply uppercase types, and the problem (that struct tags were invented to solve) is gone.
Probably I'll make something like this very soon to get started
static void *__xmalloc(size_t size);
static inline void __xmalloc_array(size_t size, size_t count) { /* ... */ }
#define xmalloc(type) ((type *) __xmalloc(sizeof (type)))
#define xmalloc_array(type, count) ((type *) __xmalloc_array(sizeof (type), (count)))
Later obviously more sophisticated allocation is needed, but the point here is how macros can be employed to improve safety and ergonomics. This is how I do polymorphism in C basically, not much more is needed than abstracting over size and maybe alignment for almost everything. In some cases, manually set up v-tables make sense from an architectural perspective.I also often make a very simple "logging" module that does basically printf logging but with \n automatically added and optionally printing out __FILE__ and __LINE__ but without requiring to re-type this all the time. Something like
void __msgf(const char *file, int line, const char *fmt, ...)
{
va_list ap; va_start(ap, fmt);
if (__debug) printf("At %s.%d: ", file, line);
vprintf(fmt, ap);
printf("\n");
va_end(ap);
}
#define msgf(...) __msgf(__FILE__, __LINE__, __VA_ARGS__)
Other than that, I like to not think about the language but about what the machine is going to do. How to decrease size of working set and generally speed up the program. How to speed up compilation (don't expose all the internals in the .h files). Things like that.There are very few container data structures actually needed, most of the time it's just C-arrays, dynamically allocated buffers (pointer + capacity), and some simple queues / synchronization primitives. Also linked lists. A favorite of mine are chunk-lists. All of these are very simple to implement -- there is hardly a point of making them in a super-general library, it's ok to code them from scratch for all but the smallest projects.
https://news.ycombinator.com/item?id=27288145 (May 26, 2021 — 94 points, 21 comments)
https://news.ycombinator.com/item?id=21093727 (September 27, 2019 — 102 points, 16 comments)
The creator of C++ himself is saying C++ is a better C and his goal was for C to cease to be and be replaced. The reasons C is not a strict subset of C++ is because some features of C are outrightly dangerous and were changed in C++ for good reason.
Most of the new features of C these days are actually backported from C++, such as atomics.
Also the whole thing about how in C you should think in terms of modules rather than classes also applies to C++. OOP remains a bad paradigm regardless of the language. And funnily enough some of the biggest C frameworks are dedicated to providing OOP in C (e.g. glib).
I think the author meant /Wall.
typedef struct { int val; } meters_t;
NOOoooooo, please stop doing this. unless you are a library author who gets to define things like uint8_t, please do not do this
> Use struct wrappers for strong typing
even BIGGER no. This is completely misunderstanding what a typedef is and what it should be used for.