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.
This kind of thing has been proposed many times over the years. It even existed back in the 1980s. I don't care much for it because it balkanizes the language into mulitple sub-languages.
I don't see what is there to balkanize. Are you against any lints that aren't formally specified or may have false positives?
If the compiler warns that e.g. you have an unused function argument, it may be a mistake, or it may be intentional (e.g. it's a callback for something that passes the arg anyway). Then you either have ad-hoc syntax extensions or dummy expressions for convincing the compiler it's used, which may fail in another compiler that defines "used" differently, or you explicitly silence the warning. Unused is relatively easy, but there's a whole category of pedantic lints for other code smells.
I've resisted adding warnings to D for a long time. A couple have crept in, due to strong desire from the D community. A warning is a strong symptom of the language designer(s) being unable to decide what the language rules should be. So they make multiple languages based on switches. That's what balkanization is.
D is a better language for having only a small number of warnings. It'd be even better with zero!
I don't think it's possible to have a language so perfect that nobody can write bad/weird code in it. There are always going to be cases of code that is technically permissible, but very unlikely to be intended, and warrant a "did you really mean to do this?" lint.
e.g. `unsigned < 0` is likely a mistake, but maybe comes from a macro/generic code that allows multiple types, or the zero is from some configurable constant. I don't see how flagging this is balkanizing anything.
If the compiler doesn't flag code smells, it's a missed opportunity to help users and catch mistakes early. OTOH if the compiler turns potentially-benign things and unidiomatic code into hard errors, then it's annoying during development and refactorings.
golang tried the no-warnings approach, and IMHO it's not working well. Sometimes it's too pedantic, but sometimes not strict enough. It sprouted `errcheck` and `go vet` to bring the warnings back.
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.
I meant that the intent is obviously the chained comparison. If lots of people do this (I have no idea) then it'd be nice to ensure we're doing that nicely, and if necessary here's syntax for expressing it (not in C though, where it already does the wrong thing).
What on Earth does your D expression do for numeric types? And why?
In Rust I can write this only if, in fact, c is always ordered with respect to a boolean which means c is itself a boolean. Since c is a boolean then ((a<b)<c) actually means ((a>=b) && c) which er... I guess? I doubt that writing either when you really meant the other will make your intent clear.
> I meant that the intent is obviously the chained comparison.
Yes, "Is `b` between `a` and `c`?" is a natural and common question to ask.
FWIW, Julia does lower `a < b < c` as a chained comparison:
julia> Meta.@lower a < b < c
:($(Expr(:thunk, CodeInfo(
@ none within `top-level scope`
1 ─ %1 = a < b
└── goto #3 if not %1
2 ─ %3 = b < c
└── return %3
3 ─ return false
))))
That is, it turns `a < b < c` into `(a < b) && (b < c)`. You can make the chain arbitrarily (within reason) long.
Of course, if a language doesn't lower chained comparisons in this way, I would much rather have a compiler error than have it silently return `(a < b) < c`.
Sure, we're all in agreement that C and C++ (and probably some other semi-colon languages) make the wrong choice here.
In Godbolt I see "missing closing `)` after `if (a < b`" from D which unlike the Rust error doesn't acknowledge the user's intent and instead is talking about grammar. Hopefully the user will go read the documentation and find out that nope, you can't do that in D, because if they just adjust their code to silence the errors then they end up with ((a < b) < c) and that does... not do what they intended.
D does it with a grammar rule, not by looking for the case after the AST is constructed. It simply won't parse. Hence the origin of that error message.
those misplaced semicolons are things i have only ever seen in extreme beginner code - "semicolon is a line terminator" school of thought. still. a warning would be good. nothing to do with the language itself, of course.
I've made the same mistake now and then. But I never suffer from it, as the compiler zaps me with a fatal error. It appears in other contexts, too, like:
well, i guess we probably all have our favourite mindless error that we keep repeating but i can't honestly think of too many that i make. mine are more at the semantic level - things like iterator invalidation in c++, which are more tricky for the compiler to spot. out of interest, does D check for that kind of stuff?
Correct. For example, if int is a 32-bit type (as it commonly is), then uint16_t will be promoted to signed int, and (uint16_t)0xFFFF * (uint16_t)0xFFFF will cause signed overflow and trigger undefined behavior.
stdint.h includes definitions for format strings for printf/scanf-type functions (e.g. PRId64, ...) for all the new types. Using them is a bit clunky (e.g. printf("%"PRId64"\n",n);), but you get used to it.
True, but "a bit clunky" is an understatement for how awful they look. I've never seen them used in the wild. I don't use them either.
What I do use (since D supports calling C printf) is have the compiler check the format string against the types of the arguments. This means it won't compile if there's a mismatch. Many C compilers do this, too.
I appreciate the intent, C has become its own thing with its own set of conventions, and people still writing "C/C++" often reveal that they don't have a clue about either.
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.
> people still writing "C/C++" often reveal that they don't have a clue about either.
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.
I think the things that particularly gets people angry are:
- When someone says "C/C++ Programmer" but they actually mean either a C programmer, or a C++ programmer, and hey, it's all the same right ? This is obviously related to the general cluelessness of recruiters. The 19 year old wide boy recruiting people for your bank will try to bring in an experienced embedded C programmer to interview for a post on your 5MLOC C++ line of business application, and they also aren't clear on the difference between Mystic Meg (an Astrologer) and Edwin Hubble (an Astronomer). It's also infuriating when people who supposedly know either language describe themselves this way. If you actually know C or C++ fairly well, you should know they're different in important ways and it's unlikely you actually know both equally well.
- When a project/app/whatever is described as written in "C/C++" but almost always this means either, "It's actually C++ but a few files are technically not using any features exclusive to C++ so I guess a C programmer could understand them" (that's just C++) or "It's actually C, but we did compile it with g++ and that worked".
If we saw Firefox referred to routinely as being "C/Javascript/C++/Rust/Python/Assembler" then I don't think you'd see as big a reaction to C/C++ but people will act as though "obviously" somehow the C and C++ in Firefox are basically interchangeable, but the Python and Javascript are not.
> 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()).
> Also, this is where it might have been helpful to go into different conventions about allocations, whether the caller or callee should allocate, etc.
There's another blog post about memory management which might have aged a bit better (at least it's less controversial heh):
On top of revealing lack of grammar knowledge, it also reveals lack of understanding of ISO papers, documentation from all companies with seat at ISO, their job boards, and major publications like the now gone "The C/C++ Users Journal".
Grice's Maxims apply. In many cases it's weird to have the "or" where we're otherwise very specific.
"Wanna go get a burger/pasty ?" seems pretty reasonable if we happen to live somewhere that a burger or a pasty are reasonable food alternatives. Maybe there's a burger van parked on the corner, and there's also a shop that sells pasties. Pizza is food, but it's not an option here.
"Wanna buy a house/chocolate bar ?" seems very weird. Why these specific alternatives? Do you live somewhere with a confectioner and a real estate business and nothing else? Even so, what sort of person isn't sure which of these things they would buy ?
C/C++ is closer to the latter the more you know about both languages.
> 2+ years of experience with one or more general purpose programming languages including but not limited to: Java, C/C++, C#, Python, JavaScript, PowerShell
> Experience in software development, using one or more general purpose programming languages (e.g., C/C++, Java, Python, JavaScript, C#, Go, Objective-C).
> The AMD Display Solutions Team is seeking a talented engineer to help develop new SW features, bring-up new graphics cards and make AMD’s driver software industry leading. This role offers the opportunity to show off your technical skills in kernel mode debugging, solving complex engineering problems and architecting new C/C++ code.
So one can be pedantic as they want, not accepting jobs that state C/C++ or whatever, meanwhile the companies that rule the world of C and C++, and their evolution, seem to know what it is all about.
In the cases where it's a list the problem is really obvious. If I wrote that I should like to buy clothing such as "Cardigans, Blouses/Socks, Waistcoats, Trousers, Skirts, Shorts" that's obviously really weird. Do I think Blouses and Socks are the same kind of thing? Why aren't they just listed individually like the others? Weird.
Notice that none of the lists write Java/Javascript or Go/Python because that would be silly, so why do they write C/C++?
Beyond that, take the ARM job. You'd have us believe that description says C/C++ meaning either C or C++. But it's an LLVM developer job. It says C/C++ because the person typing it up didn't think there's much difference.
Maybe ARM is open to hiring people who don't actually know any C++ for this position, there's no salary indication and it's not highlighted as a senior role, so perhaps they're hiring fresh graduates whose C++ would be terrible anyway, a few weeks of Kate's Pluralsight courses and reviewing other people's contributions might get good non-C++ candidates up to speed. But I wouldn't bet on it, applying if you aren't a C++ programmer is likely a waste of your time.
For the AMD one I can actually believe AMD would take a great C developer. They need Linux kernel support just as much as Windows, and unless AMD is going to start writing in Rust that'll be C code. If you send Linus code that thinks it's basically all the same he's going to have a meltdown and make your life miserable.
But for Apple we're back to I just don't believe C candidates will get this job. That's C++ software again, they just aren't outright admitting it isn't a C job, but once it's apparent you are a C programmer you don't meet their actual needs. Their mistake, not yours, but it's your time and effort.
I have no special insight, but in my experience employers often have a significant gap between what they intended and what is achieved during pre-hiring contact with prospective employees, as employers get larger and roles less specialised everything is going via HR. So if HR writes C/C++ there's every chance that the actual people you'd work with wish it wouldn't.
About a year after getting my current role we were asked about our recruitment experience, me and also people who'd arrived more recently. I pointed out that they use Outlook so much as a Microsoft shop that they'd actually sent candidate interviews out as internal Outlook appointments. Everything looked fine to them, "3pm Friday, Interview tialaramex" but er, I don't run Outlook, so I just have a blank email telling me that the interview is booked. When? Where? Who with?
Now, obviously this is not my first rodeo, so I inspected the email, and internally it has a bunch of Microsoft flavoured vCalendar or iCalendar or whatever. But because Microsoft's idea of "timezones" is nonsensical (No Microsoft, "GMT" does not mean "You know, the time in London" just stop doing that) I emailed the HR contact to confirm I have the time right, and of course the HR contact screwed up. So, even though the people doing the interview have "3pm Friday, Interview tialaramex" on their calendar, the HR contact says, "It's 2pm Friday. Thanks for your enquiry" which then added even more confusion.
Anyway, all of this was completely invisible from inside the organisation. And apparently some other candidates in my pool had similar confusion. Do they lose candidates this way? Nobody knows for sure. Ouch.
I think they’re referencing how English grammar isn’t consistent, but at least C and C++ have a defined grammar (from the spec). Therefore, people bashing C and C++ should look in the mirror?
On the "no formal parameters function" - You can forward declare the function with the parameter list, and then define the function without a parameter list. This should (might) generate a compiler error/warning in very old compilers, but really it is an indicator that you are trying to use an older style of declaration with "modern" convention.
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."
That's by far the least interesting usage of RAII and also, annoyingly, the only one the article discusses.
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)
That is only a small fraction of RAII. The trivial fraction. If malloc fails, real RAII gives you an exception, which you may catch somewhere convenient. Real RAII runs a constructor. If construction fails, you get an exception. Lacking those, it all has to be done and checked by hand on the spot, and is often wrong, because not tested.
Not sure that's the part of C++ I'd heap praise on. For one thing, real RAII invokes the destructor, not a constructor. If your constructor throws, the destructor isn't going to be called. If your destructor throws, you're one passing exception away from std::terminate. Plus malloc doesn't throw, though new sometimes does.
Or, construction could return an optional/maybe style thing that you branch on, and then you don't need goto (sorry, non-local come-from with pretty branding) in your base language.
The important part of RAII is that the constructor actually acquires the resource.
The fact the OP insisted the important part of RAII is not that but running destructors shows he hasn't understood the most important concept in C++ programming.
The destructor is the distinguishing feature of RAII. Lots of languages have constructors without RAII because they do not have deterministic destructors. Examples are Python and Java.
The downside of C++ is that exceptions can come from any possible function call depth, whereas in C you only have to deal with error return codes. So in that sense it’s easier to deal with errors in C, and harder to test the error paths in C++
That is the upside. You don't need to know how far down it happened. If you need to know more, you catch it lower. But most often you don't. Whatever the hell it was didn't work, and you do whatever is called for, then.
Exactly. Being forced to handle errors at every function call sounds great, but then you end being forced to bubble up errors you don't care about like Go does:
res, err := func()
if err != nil:
return nil, err
That's not actually handling the error, and exceptions save you from being forced to pretend like you are. Just let it bubble up into the catch block further up the stack.
You don't have to code like that in Go, C or other languages without exceptions or fancy option-type sugar.
A sensible thing that often works great is to have a sticky error state (either a single int, or list of stuff you append to) then you just keep calling functions which will append to and/or replace the current error state until you reach a point where you can/care about handling errors, then you examine the persistent state and do something about it.
I hope you are not referring to things like errno? Because it, just like a returned error, must be checked after every call that might set it, before you know if you can safely proceed with the next.
Otherwise, what happens when you keep calling functions and there already is an error present? Are all functions implemented with if(errno) return null guards at the top? That's putting a lot of trust into global state and library writers. How do you know which functions become noops and which continue working in state of error?
Additionally, that would be a debugging nightmare, because if you keep calling functions before examining the error, how do you know which call introduced the error first?
> how do you know which call introduced the error first
You don't. You can't. You don't want to.
Take read()/write() for example. If you look into the kernel, it's physically impossible to name a function call that "introduced" the error. If you do a write() that will simply copy some memory into the buffer, and the syscall returns. When pages are flushed out to storage later, an error might be reported from storage asynchronously. The error is bubbling back at some point, at some syscall related to the file, but the error has nothing to do with that syscall necessarily. The error you get back could even be "caused" by a write to the same file but from a different process.
So it's perfectly reasonable that the FILE API, which wraps read()/write(), simply stores returned errors in the FILE Handle. Distributed systems are a perfect application for objects that do error isolation.
Delayed error ack is a completely orthogonal issue. Only necessary as a performance workaround, both in the case of unflushed disk buffers, sockets and distributed systems.
Parent presented sticky errors as an effective substitute for exceptions or error codes. Delayed errors is not a way to organize error handling easier, which I believe this topic was about. Delaying the error ack has quite the contrary effect, fail fast whenever possible will always be more accurate. How that surfaces to the caller is the more relevant question.
What happens when the disk is full or disconnected when copying 4GB src file to dst file after 100MB progress? (Yes, this error might occur slightly delayed due to buffers.) You surely don’t want to continue reading the remaining 3.9GB source file and call write() in noop-mode another thousand times in your loop before realizing this error on flush. Adding manual flushing just to check the error both negates the performance from the buffer and introduces extra complexity for a simple error check. Hence, every individual write must be checked regardless.
Such buffers are not infinite either. What do you do when write() fails because the buffer is full (EAGAIN)? Again back to square one of checking each individual write call instead of only checking the final error of flush or close.
If you look around there are lots and lots of objects that are "distributed", or aren't but should be. Synchronicity is often what's killing performance and introducing complexity.
> You surely don’t want to continue reading the remaining 3.9GB source file and call write() in noop-mode another thousand times in your loop before realizing this error on flush
It can be completely reasonable to back out only at strategic points. Copying a few KB or MB of memory more will rarely matter for an error case that shouldn't be optimized for. If there is an error, you'll typically want to reset a larger context object anyway. It depends on the situation, but by not having to handle the error at first notice, you can sometimes simplify the logic.
> What do you do when write() fails because the buffer is full (EAGAIN)?
EAGAIN is a different beast, it's not a "real" I/O error. With better APIs you retrieve buffers first (often in a different phase), removing this class of errors completely. But you can mostly just ignore EAGAIN anyway. It's a transient error (or not an error at all, really) that simply tells you the reason why zero bytes were written.
With fwrite(), not sure if it is well specified how it should interact with non-blocking FDs and EAGAIN. Probably it doesn't even allow you to distinguish between EAGAIN and I/O errors. It could also be an option to return a short write in this case (but I believe fwrite() needs to set either the error of EOF flag if it returns a short write). I also think fwrite() is largely not used with non-blocking FDs.
Yes like errno, except not global of course, you add one per struct/context/module/thread/whatever.
And you design the functions to early return if the error state is set.
>how do you know which call introduced the error first?
You rarely care about that but if you do you either make the error state stick to the first error, or as I said, make it a list you can append multiple errors to.
"Sticky states" work too; IIRC, it's how FPU exceptions on the x87 work. But I was responding to the upthread comment that "exceptions can come from any possible function call depth, whereas in C you only have to deal with error return codes." Sticky states still have that "issue." After all, they're just an alternative to try{}catch{} blocks.
Rust, anyway, captures this foolishness in a macro, which saves lines of source code, but still costs cache footprint, branch predictor slots, and runtime.
The Rust try! macro, which I assume is what you're thinking of, has been obsolete for many years - you can still refer to it because the Rust compatibility promise is taken more seriously than in C++ but you will need to use rather awkward syntax to get at it in modern Rust editions since the keyword "try" is reserved since 2018 edition.
These days you'd use the Try operator ? which is not a macro, it's an operator.
Try is really interesting, it's a unary operator so it takes a single parameter and it maps that parameter into a control flow decision. For Result the effect is similar to what you got out of the try! macro, but of course this operator can be implemented on any type. The standard library provides six implementations, including famously on Option, but also on ControlFlow itself, which is pretty nice.
This means across a complex system you can choose to collect Results, and decide what to do about the Results later, (perhaps after you have all of them, or after you've a certain amount) or you can choose the same for the ControlFlow decisions resulting from those Results.
You can also turn things on their heads, and decide that what you want to do is return early on success, but continue processing for errors -- which is something that's just unthinkable in an exception world where control flow and success are somehow the same thing. Rust took some years to figure out that's just not true which would be embarrassing if the assumption that it's true wasn't baked into the entire C++ language.
You still wouldn't want to throw for the success case, so the point is meaningless. And of course you can return early on success in C++, and continue to process details further otherwise, with zero difficulty, so there is no cause for embarrassment.
Rust people should be embarrassed if they carved out something special for this niche case.
> And of course you can return early on success in C++, and continue to process details further otherwise, with zero difficulty
Alas, C++ exceptions don't permit this, on failure the exception will get thrown and control flow switches away without any opportunity to intervene. That is in fact its whole purpose. That's the design mistake, it's not something you can fix, it's a choice which seemed clever last century, and it's last century's design.
I'm guessing you're thinking of capturing and hauling around exception_ptrs ? But that's far from "trivial" - it involves needing to understand how the temporary objects which were constructed in some "unspecified storage" work and then wrangle this custom smart pointer. In contrast Rust's Result and ControlFlow are just ordinary sum types, no magic involved.
There are two sum types here, and the fact they're different is the insight.
The try! macro didn't have that insight, and the first attempt to make a Try operator didn't either, but the current one does. This sort of experimentation is not available in practice to C++ but that would only slow it down a little, what prevents forward motion far more is that WG21 doesn't want to learn from other people's experiments.
C++ 23 gets std::expected which is, modulo IFNDR nonsense, a Result type. But C++ 23 doesn't have, and none of the further papers propose, a type analogous to ControlFlow.
I actually wondered how C++ 23 does the equivalent of Iterator::try_fold without ControlFlow, how do they express this idea? Did they use std::expected here as once Rust used Result ? The answer seems to just be "They don't" which I think gets to the heart of it.
A resumable, short-circuiting fold would be just as useful in C++ as it is in Rust, but it's easy to express nicely in Rust and doing so in C++ would insight anger from Exceptions purists, so that likely won't happen.
“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.
As an avid long-time professional C++ user, I agree with that paraphrase. At the same time, I know that (somehow!) people use and like C. I’ve been looking for an article like this explaining how such a to-me-antiquated-feeling language (C) can actually be used and liked. This gave me a glimmer of understanding. I’d love to see more articles like this.
Freeing stuff allocated in the same scope is both easy to do and easy to catch when debugging.
If you have multiple returns just use goto FINALLY instead of returns and put the deallocations before the return there.
I do a bit of embedded C, 32kB for combined heap and stack, that sort of thing. I wonder if the article author heralds from the same space. I pretty much found myself going yup yup yup for most of this. But I also know that when I move into higher powered/level domains, I don’t keep using C.
C’s advantages over C++ don’t have much to do with language features. I don’t know what the author was thinking here. Any diehard C++ user is not going to be swayed by “fewer features”. Why not write an article that gets into the actual strengths of C?
This is a mostly theoretical argument in my experience. It's quite easy to close the things that you need to close (if there are very many, something is wrong).
RAII can improve "scripting" speed when putting a lot of automatic variables on the stack. However, a lot of those variables need to be moved to the heap when proofing out the code, and that consolidation isn't going well because RAII doesn't work with generic code (void-pointers, memcpy etc). You need to go all-in with RAII containers. Which causes a lot of boilerplate and increases compilation times.
You appear to be using "generic" to mean "C-ish". That is not what the word means, in context, and there is no reason to do any of it anyway. "Proofing the code"? Does that mean anything at all?
For RAII on heap objects, you have std::unique_ptr, and no boilerplate. Anyone failing to lean into RAII to manage resources is just choosing to write unreliable code. Or C, but I repeat myself.
Due to language limitations, a lot of C programmers make all their variables shared pointers, which means they don't have to figure out how to do value semantics, moving and copying objects, especially in multithreaded environments.
Interestingly, it seems Rust programmers do the same too, because otherwise programming in Rust is too hard.
> Due to language limitations, a lot of C programmers make all their variables shared pointers, which means they don't have to figure out how to do value semantics,
I wouldn't approve. Make shared (i.e. ref-counted) pointers when actually needed.
Writing C is best when not mimicking bigger languages (i.e. with GC), but when figuring out how a system can work optimally.
Your theory is that Rust programmers "make all their variables shared pointers" ?
Even if I generously assume you're thinking of C++ "shared pointers", and so you've concluded that Rust's Rc<T> (a reference counted T) is basically the same thing, I don't see where you'd come to the conclusion that Rust programmers do this for "all their variables" as this seems wildly unlikely.
Just look at the code in cargo. (A)rc is all over the place.
It's essentially a cop out to not have to think about ownership and lifetime, and not have to worry about the borrow checker.
The fact that this is so pervasive throughout Rust code is for me a sign that Rust failed to deliver on its initial promise. The borrow checker was meant to be its main value added. But heh, people still find value added to Rust otherwise.
Hmm. So the code I happened to have open was Aria's "Cargo mommy" which has neither Arc nor Rc anywhere, but is only a toy.
So I did go look at Cargo itself, but unlike you although I do see some use of Rc and fewer of Arc it was scarcely "all over the place" when I looked and in the cases I spent any time actually thinking about it's a shared value, so, yeah, that makes sense.
I randomly looked at cargo/core/profiles.rs and cargo/core/registry.rs and cargo/core/compiler/compilation.rs without finding either Rc or Arc used in those files at all. Searching across the whole repository I found some places which do use Rc, and fewer using Arc (implying this data is shared between threads) -- but this doesn't really support your original claim does it?
> It's essentially a cop out to not have to think about ownership and lifetime
Not a cop out. Something very much like Rc<> is needed whenever an object's lifecycle might be independently extended by multiple referencing "owners", none of which is subsumed by the others. That's a matter of broad high-level design that can't generally be avoided, least of all in a multi-threaded context (as shown by the use of Arc<>).
> "Proofing the code"? Does that mean anything at all?
Since you're already schooling me on the meaning of terms, you probably have enough experience to have realized that the typical code begins life in a stubbed out form that is barely functional enough to deliver first results. It is then iteratively enhanced and made more widely applicable, more robust, more refined, better specified, more performant, and so on. In the process, the code is undergoing many revisions, and moving data structures from automatic storage (stack) to the heap is very typical.
Another word I'm thinking of for this process is "consolidation", which I have also used. My apologies if I don't speak in your terms / in the most precise terms.
I don't have experience of this "moving from ... stack to the heap", in C++. Correct code is the same wherever the objects live, whether stack, member, or heap. Changing it costs time and adds faults.
This proofing process is another example of what makes coding C more costly and buggy than coding C++. I did it for years, and miss it not at all.
If writing systems-level code solving for non-trivial requirements, the majority of the state has a lifetime that outlives any particular function call. That's when you need to move that state to the heap.
There's no clear scope anymore, ownership has to be moved from function scopes to RAII managed container structures. Things are quickly getting more complicated and expensive (software complexity/bloat, compile times, binary size...) when you have to nest your structures in templated containers that don't know what you're nesting in them...
This is where all the complexity with 17 constructor types and 99 move semantics with const, non-const, r-valued references and abstract virtual base classes came from, and there might be less than a dozen persons left on the planet who really understand it all...
With RAII, it is trivial to move things from stack to heap, or to a member of something that might be on the stack or heap or in a container, without changing any of the code that implements the object. Most usually you don't need to declare or define any constructors, assignment operators, or a destructor; the compiler provides them, and guarantees they are correct. (Sometimes you provide one constructor as a convenience for users.)
It has been many years since I found a use for an abstract virtual base class.
Literally millions of people understand it all, and use it every day with no difficulty. You don't need to invent difficulties, or waste days "proofing" code that may be written exactly once and never touched again.
To clarify, I think by “move things from stack to heap” you mean “move values from stack to heap”, where, e.g., `std::vector<int>` is a value. The vector’s data is still on the heap (or in the allocator’s pool for `std::pmr::vector<int>`) but the value in the sense of value-semantics is moved from stack to heap.
I guess what they mean is moving to static memory allocation, not to the heap. That is something done rather frequently when proofing embedded C code, because it's simply to easy to mess up manual memory management. Only reason to move from stack to heap would be for huge data structures not give stackoverflow, something that can be hard to test for.
"It's quite easy to close the things that you need to close"
Very true. It is easy. Not difficult. That doesn't seem to stop enormous amounts of C++ code being created in which this is simply got wrong. Just because it's easy doesn't mean that huge numbers of programmers won't still get it wrong. They do. Regularly. If they adopt RAII conventions, they screw it up far less frequently. Those are the facts.
I prefer reducing complexity instead of hiding it in language cleverness. (Because hiding complexity doesn't make it go away). Try adopting ZII (Zero is initialization) and context managers (pooling, chunking, freeing in batches etc.), this can reduce the amount of boilerplate to a minimum. Writing generic code like that (down to the binary level) also helps improving many other metrics like executable size etc.
> Because hiding complexity doesn't make it go away). Try adopting [..] context managers (pooling, chunking, freeing in batches etc.), this can reduce the amount of boilerplate to a minimum
"don't hide complexity in small, single-purpose, composable containers, instead hide complexity in hulking behemoths which can consume months of refactorings & iterations" ?
That's not simplifying your code to be generic. That's adopting a particular framework & coding style. Which yes, you have to do in C, but you're doing that because of language issues. You're not saving yourself from "language cleverness"
That's nice, and I'm sure your code is just lovely, but there's just one of you and millions of people writing C++ who can't do that but can just about manage some simple RAII.
One can do lots of things with RAII. I once wrote some classes to generate xml. I used constructors to write opening tags and destructors to write the corresponding closing tags....
I used to do `if (errno) goto err;` a lot. I eventually realized that nested functions did the job much nicer:
void err() { printf("oops, we failed"); }
...
if (errno) return err();
This cleaned up a lot of my code. (The optimizer would of course inline the function, and the common tail merging would automatically convert it to the goto version in the optimizer. So this was cost-free.)
Why C doesn't officially have nested functions is, well, that's C!
Cleanup goto-style is hard to describe as a "rat's nest", since all the gotos in the same function go to the same label at the end that just does all the cleanup before returning. There's nothing clever about it, and many codebases wrap it into macros and such to make the syntax more concise and the intent explicit.
In general, any random C compiler is likely to not support that feature, because the way it interacts with function pointers makes it unnecessarily complicated.
As for function pointers, a simple solution is to not allow them unless the function is declared `static`. `static` functions don't have a hidden static link.
The problem isn't doing it as such; ALGOL 60 could do nested functions just fine.
The problem is doing it in an ABI-compatible way when you already have an ABI. The gcc implementation of nested functions does that - they are compatible with regular function pointers - but at the cost of requiring executable stack on at least some platforms.
And for C compilers that aren't gcc, the question becomes: why partially implement a non-standard gcc feature?
Yeah, stopped reading after that. How wrong does he want to be?
It is hard to tell what the point is of this article is. Yes, the C-ish subset of C++ differs from ISO C, in trivial details. That is irrelevant to C++ users, who may consult ISO C++ to discover the actual subset. They interact with any C only when they #include a header for a 3rd party C library, and then the relevant subset is what, exactly, appears in that header.
There is really no legitimate reason to code C anymore: A C++ compiler is available for any modern development target. Confining yourself to any C subset (with or without ISO C marginalia) amounts to crawling when you can fly.
To be more specific: nothing that can be done using C is unavailable to the C++ coder, but enormous power is available to the C++ coder and wholly unavailable in C.
For me the legitimate reason to code C is to get stuff done, period.
There is hardly a need for more (to do what I do -- systems in the broadest sense) and there's a lot of time saved by not thinking about 21 "safe" and "ergonomic" ways to write everything in C++ (that very often end up being not that safe, and unintelligible).
Yes, there is the "subset" argument, only use what you need. But I don't buy that, I'm not like that, it doesn't work for me. Constraining myself makes things easier.
I started in C, and am still in it for systems/OS development at work.
In UI development, we had older C toolkits, but are replacing them with newer toolkits that are C++. I started off trying to just do C business logic with some C++ glue code for the UI. Sticking to what's familiar. In the end, it was just easier to do everything properly. All C in areas that are C. All C++ in areas that are C++. Same with C#. Trying to mix them is just more work than necessary.
At least I get to add C++ to my resume. There is a little joke at work, w.r.t. our existing codebases and code choices. They are career-driven choices, not technical choices. The result is a mess of a codebase, but I'm fully embracing that now.
> For me the legitimate reason to code C is to get stuff done, period.
If I want to just get stuff done, C++ is great for that. Don’t underestimate how much you gain by having string, vector, and map when you just want to crank out code.
Nobody is forcing you to overdesign stuff. You can always just crank out the code you want.
Yeah, just having the standard library container types (or similar generic third party libraries if you don’t like std ones, eg I tend to use phmap maps and sets instead of std::unordered_map/set). Templated containers just make using them so much easier than generic containers in C. Ergonomics matter when you just want to get things done. Plus the C++ containers make it easier to manage memory IMHO.
The main reason I still sometimes use C is compiler support for microcontrollers where everything tends to have a C compiler but not everything has a C++ one (or if it does, not necessarily an up to date one).
I don’t use either in my day to day really but I do have sone personal projects. Ultimately I foresee doing these in Rust where I can, but for stuff like hobbyist game development, I’m currently too bought into some of the C++ libraries, primarily EnTT, but even there I think I’ll eventually just end up using Rust with Bevy. For now though, I actually quite enjoy using C++17 (and maybe 20 soon if I find the free time).
Even something like Turbo C++ for MS-DOS is preferable to raw C, the only embedded toolchains that don't offer even that are usually stuff like PIC and similar.
Which, in any case, have companies like Mikroe selling Basic and Pascal compilers.
> There is really no legitimate reason to code C anymore: A C++ compiler is available for any modern development target. Confining yourself to any C subset (with or without ISO C marginalia) amounts to crawling when you can fly.
>
> To be more specific: nothing that can be done using C is unavailable to the C++ coder, but enormous power is available to the C++ coder and wholly unavailable in C.
As a counterargument, sometimes constraints can be valuable in their own right rather than just being requirements for some larger goal. The most prominent example of this in programming design is type checking; enforcing type checking at compile time strictly reduces the number of programs you can express, so dynamic languages can do everything that static languages and more, and yet it turns out that it's still pretty useful specifically because we sometimes want to avoid the programs that a dynamic language would allow but a static language would reject. At the risk of getting too philosophical, imagine framing programming as an exercise in trying to write rules to specify one program out of the set of all possible programs, where each bug you write that changes the semantics moves one step further from your goal. The compiler can act as a "filter" on the set of programs you can potentially select by refusing to process your rules if they would specify one of the forbidden programs, which reduces the chance selecting the wrong program by mistake. In this scenario, the theoretically ideal compiler would be the one that rejects all programs other than the exact one you want! This obviously isn't possible in the general case, but the point here is that as long as compilers don't reject the exact program you want, a compiler that rejects more programs is _more_ useful.
I'm not making any claim about whether it is in fact worth it to code C anymore, but I disagree with the logic that C++ letting you do anything C can do and more is proof that C++ should always be used over C. Maybe C is crawling compared to C++'s flying, but if my goal is to get a pen that falls under my desk, it's a lot more useful for me to not use an airplane to do it.
I think the gp was talking about anti-features. Austral's announcement included a list of features it avoided on purpose. Including several from c++. operator overloading, increment operators and even implicit oder of operations were listed as features that made codebases worse, even if no one has to use them.
Anyone who says C is not important hasn't worked on embedded systems or operating systems.
I work on embedded systems. There are roughly 3 choices for languages[1]. C, Ada, and C++. Except the version of C++ is sometimes often vendor specific and may lack features that are "standard C++", and never have a "full" STL. In addition, if you're not super careful with C++, what you thought was a copy or declaration can execute additional constructor or assignment logic. It might just interfere with your estimate of how long something can take (which is super important in interrupt handlers), or worst case, smash your stack (which can be as small as 1-2k). With C, you pretty much know what's going to happen and what's getting called. The fact that C doesn't require a runtime (nor does Ada), means you can use it to write kernels. You can't do the same with a 'full' version of C++, so you're back to a cut down C++.
I also spend time debugging compiled, optimized code by tracing instructions. This is hard enough in C, where I don't magically jump to a constructor function or an assignment function. I can visualize in my head how the C code could look like, given the instructions the optimizer produced. So it may not look like my code, but it is a version of the code that I can at least logically infer.
Ada is nice, but outside of some super-safety critical realms, it hasn't had the uptake. Private industry prefers not having to make the investment and the government let everyone waive out of it.
1. I know people are going to to say 'what about Rust, Go, or embedded something or other?' They are not as mature in their development lifecycle. So there's no validated RTOS for Rust (there are some experimental ones). Go is even further behind. Basically, you need a vendor supported RTOS that's validated for the safety and operational requirements of your use case, before you can be a serious choice for a lot of embedded work.
> Anyone who says C is not important hasn't worked on [..] operating systems.
Most OS code is written in C++ or Object-C, not C. Between Android, MacOS/iOS, and Windows there's not a lot of C code. The kernels are most of it. The NT kernel has C++, but hard to get a source on how much is still C or not. But the kernel is far from being most of the OS regardless.
> The fact that C doesn't require a runtime (nor does Ada), means you can use it to write kernels. You can't do the same with a 'full' version of C++, so you're back to a cut down C++.
For a huge number of users, "full" C++ is C++ with -fno-exceptions and -fno-rtti anyway, at which point the 'runtime' is like 2 functions and it's absolutely perfectly fine to use in a kernel. But regardless neither of those features are inherently incompatible with being in a kernel. You just have to implement a runtime to do that, just like kernels written in C have to implement a libc replacement.
Unless you're talking about the standard library (even though nearly all of it is completely kernel-compatible out of the box), but then you'd have to include libc & friends in the "C runtime" category and then it's equally impossible to use in a kernel.
Sorry, I didn’t mean the userland stuff above the kernel, which, along with the boot loader, is the part that is really restricted in any sense. With the standard library, there are large parts that are not kernel safe. That’s because they rely on services like memory allocation, which are different in the kernel. Or they rely on kernel services, like files. And what I work on has no kernel, just an RTOS.
And C in the kernel isn’t really a libc replacement. They often have different behaviors because they need to run without allocating memory, or be interrupt safe. For example, in some situations I use a function call instruction that specifically does not creat a new stack frame.
Very little of the standard C++ library is not "kernel safe". If a bit of it might need to allocate, you may provide it a kernel-grade allocator to use; but you probably use custom kernel containers, for other reasons. If you don't want any filebufs, you do not make them.
I love how usually not having support for full ISO C++ or it not being up to date is an issue, while having to deal with a cut down version of C, freestanding, a custom library and compiler extensions is a plus.
It's been decades since I worked with Ada, so I could be wrong, but you could build it with an embedded runtime that allows you to do tasking and (I think I remember) exceptions. But it also has a language standard way to create limit certain features. The difference is the C++ is a grab bag, depending on the vendor, platform, RTOS, etc.
Vendor compilers are common, if not typical, for embedded and safety-critical systems. It's not masochism since they work, and in my experience they work quickly to address any compiler bugs you may discover and report. This is also true for Ada, C++, and Fortran in that domain.
If the hardware is exotic I guess you’d have no choice.
But for security critical don’t you run the risk of relying on obscurity rather than security due to the niche-ness of your stack?
What does a vendor compiler do or do better than a compatible generic one?
When you get a critical system certified for fielding you aren't just certifying the source code, but the actual executable and build process and test process and other things. This requires reproducibility for years to come. Choosing generic compilers may work in dev and parts of test, but not for actual deployment as a consequence (or it doesn't work well). Suppose you picked clang 11 several years back. Now you need to do an update to the system, you can still use clang 11, but not clang 14 at least not without doing a comprehensive recertification process. Also, if an issue is discovered in clang 11 it's likely been fixed in clang 14, but again you have to get your system recertified with clang 14. And that's if the issue is fixed, it may still exist.
With a vendor supplied compiler you can say, "We're using version 11.2". A year or two later an issue is discovered, the vendor will backport a fix to 11.2 giving you 11.2.1 which is much less effort for recertification. You aren't depending on the kindness of strangers (a terrible strategy) because you're actually paying someone to do the work.
Some vendors base their compilers on GCC or Clang. But if you're not using their provided compilers, you either need to 1) be willing to shoulder the expense of making changes to GCC or Clang for whatever you're adding to the silicon in terms of instructions, and 2) be willing to get your combination of RTOS and compiler re-validated for the safety or operational standard on which you need to deliver.
But even if you do use GCC or Clang, it doesn't change the mechanics of C++ as a poor language for embedded or operating system work. It just means you have the same choices (e.g. limited support for containers and strings, no exceptions, limited smart pointers, etc.) and you're making that choice based on GCC or Clang's limits.
It's not impossible. Other people have done it. Some have done it with a minimal kernel or micro-kernel in C and then services on top of that in C++. Just doing a quick perusal of their code base, the low level stuff is functions and structs. So, yes, it is a .cpp but not that different from the code you'd write if it were a .c. And it appears they're turning off exceptions and the standard library (which is completely reasonable). They probably have some additional coding standard (internally) like you can't use anything with a constructor in certain contexts, etc.
And on a validated RTOS - it might be in C++ under the covers. The examples that come to my mind aren't. But the world is a big and magical place, so I can't say for certain on every RTOS. The F35 uses C++ and a set of very restrictive internal coding standards built on https://en.wikipedia.org/wiki/MISRA_C.
And no one will die if their copy of Serenity OS crashes.
The rewrite in C++ movement was lived in Usenet flamewars, and we were in good path with all the userspace C++ frameworks in OS/2, MS-DOS, Windows, Apple and BeOS until the GNU Manifesto came around and urged everyone to write FOSS code in C.
"Using a language other than C is like using a non-standard feature: it will cause trouble for users. Even if GCC supports the other language, users may find it inconvenient to have to install the compiler for that other language in order to build your program. So please write in C."
Yes but back then you could compile C with a C++ compiler and get the same type checking as in actual C++. There were a few pitfalls but a shared "Clean C" coding style was viable and that's what most of the rewriting effort was focused on.
C++ did add stronger modularity with its public and private member specifiers, namespacing etc. but that was mostly useful on larger projects.
Stuff written about C++ before there was an ISO Standard obviously does not apply after. And, we should know by now how much Richard Stallman's prejudices are worth.
By all means, GNU Coding standards from 2006, 8 years after C++98.
"When you want to use a language that gets compiled and runs at high speed, the best language to use is C. Using another language is like using a non-standard feature: it will cause trouble for users. Even if GCC supports the other language, users may find it inconvenient to have to install the compiler for that other language in order to build your program. For example, if you write your program in C++, people will have to install the GNU C++ compiler in order to compile your program.
C has one other advantage over C++ and other compiled languages: more people know C, so more people will find it easy to read and modify the program if it is written in C.
So in general it is much better to use C, rather than the comparable alternatives."
You emphasize my point so clearly, I need add nothing.
But I will note that GNU Gcc and Gdb are both C++ projects now. Gold, the current GNU linker, started out C++. Are there any other still-relevant GNU projects?
That is now, we were talking about when GNU/Linux started to be relevant and what triggered language choice, versus what was happening in the desktop PC world.
The latest version of the GNU coding standard is much more permissive.
It is still far from clear whether anybody will be learning Rust, in ten years, instead of whatever is the new hotness then. Hiring a domain expert who also knows Rust is generally impossible today, and will be for, at least, a long time. So, starting a new project in Rust is OK for a project you know won't matter, but absurdly risky for one that will.
C++ is mature. None of the above is a concern, for C++.
In terms of adoption by pre-existing large players in the industry, Rust is already ahead of where Ruby was at its hype peak. It's just not quite as shiny because that code is not running some web app that is immediately demo-able, but some boring OS infrastructure stuff.
Given the money already invested into internal tooling and infrastructure specifically for Rust, I just don't see those large companies suddenly dropping it. Regardless of how good the language itself is, the sunk cost alone makes it hard to turn ship. And the language is good, so it'll use that time to entrench itself further.
They won't drop the language: whatever products depend on it will continue to depend on it. Unless hiring gets difficult; then they will transcribe it to a more mainstream language. In the meantime, language choice for new projects will be driven by experience hiring into these and other projects.
Other, newer languages will come up continually, siphoning off the most mobile who have begun to find Rust familiar and pedestrian.
Domain experts are focused on their domain. Becoming a beginner again is a recipe for radically reduced productivity. The people eager to learn a new language on the job are generally those who have not invested much in anything.
A very good article. I think it's nice as a C++ dev to see how problems can be solved within the restraints of C, and how we might learn from those limitations where C++ might have strengths and where we might wanna lean on simpler C mechanisms.
In general I agree that the C standard isn't really relevant for day to day work and that it matters more what compilers actually implement, but a C library author needs to follow the users, and that means at least supporting GCC, Clang and MSVC (and the MSVC C frontend follows the C standard much more strictly than GCC and Clang).
I'm in a job where I was taught modern C++ from the beginning, and I don't have the C89 familiarity that the author assumes here. Anyone aware of a good resource to learn C coming from a modern C++ background? I'm interested in learning it for hobby embedded systems projects.
> C++ is also not a ‘replacement’ or a ‘successor’ to C, it’s a fork which slowly ‘devolved’ its C subset into a slightly different dialect of C without much hope that the two languages can ever be united again (which IMHO is a damn shame, because being able to mix C with a sane subset of C++ would be really useful for writing libraries).
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.
> in C this function takes any number of arguments ... I guess it’s a leftover ‘syntax pollution’ from old K&R style function declaration syntax). ... Instead in C, declare the parameter list explicitely as ‘void’ so that you actually get compiler errors when accidently passing arguments to ‘my_func()’ ...
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.
> Now you have a named struct bla_t which is typedef’ed to a type alias bla_t, and the named struct can be properly forward-declared.
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.
Anyone know of a better blog/reference for why to use C over C++ today? I'm still writing some of my own stuff in C but honestly I think that's inertia. Freestanding C++ with the exceptions turned off would work fine.
I don't know of a reference, but C encourages to do more things at run time, whereas C++ encourages you to do more things at compile time. Simplest example of this is probably qsort() vs. std::sort(), but it extends far beyond this. So you end up with slightly slower code in C, but much faster compile times, and much smaller binaries. So if you want smaller binaries or faster compile times, you'll have an easier time keeping them in C. Of course you'll have an easier time keeping & introducing more bugs in C too, but that's another matter.
There are only two valid reasons, UNIX/POSIX kernels that naturally will hardly embrace anything else, and embedded toolchains that refuse to embrace modern times.
I recently started a project in pure C (coming from writing C++ professionally for a few years). I love it. Honestly, the biggest advantage for me is the massive reduction in mental overhead. In C++, every single line introduces something to think about: is this a reference or a value? Does this incur a memory allocation? Is this copy expensive? Has this been moved from? It’s all a huge mess.
Contrast with C: none of these problems exist (well, maybe the copy one if the struct is big) I can focus on implementing my design, even if it does take a bit more typing.
I spent a few years coding C after using C++. The cognitive load needed to keep code correct was crushing, and switching back to C++ was massively liberating.
I didn't code any memory leaks, use-after-frees, or buffer overruns, but the effort to avoid them was ever-present, a continual drag and distraction from what the code was meant to achieve.
I guess it really depends on what your program is doing. In my system, everything I do is linked to how I use my memory and its layout (so much that I don’t use malloc/free, just my own arena allocator), so I wouldn’t be able to avoid thinking about things like this in C++ either.
I can see how if you have allocation-heavy code that making them more convenient to use correctly would be nice, but for me “allocation-heavy” is an anti pattern anyway.
Simplicity. You can keep the entire language in your head and everything is explicit with no "magic" involved.
That said, you might want to start moving to C++ slowly, first as a "better C with user-defined types" and then to OOP/Generic/etc. programming goodness which gives you a much larger design space.
Right, you can "hide" what's going on in otherwise proceedural C codebases w/ the preprocessor.
To be more charitable to OP, in C++ you can further "hide" what's going on with destructors in addition to the preprocessor.
(I like RAII and hope more C programmers can learn to appreciate it. I have fixed and reviewed many fixes in the Linux kernel for -Wsometime-initialized because error handling using goto is a constant source of programmer mistakes introducing UB).
I'd love to chat sometime about approaches to implementing a CPP!
It's not even that macros can hide complexity. It's that the specification for the C preprocessor is itself rather complicated and non-obvious - e.g. what happens when nesting even simple macros, or even something as basic as tokenization.
There's pretty much all the same compiler magic either way. At least C++ gives you placement new and std::launder, C gives you malloc and you cross your fingers that the compiler's alias analysis goes your way for now.
Learning C after working on high level languages I really like the simplicity. Though, I wish there was a C that had a few features from high level languages - or maybe an enhanced C that would compile to C.
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.
I haven't used C++ very much, but I found "A Tour of C++" to be a great introduction. It focuses on the core of C++ OOP semantics and is written by Bjarne Stroustrup himself. I like the book because it doesn't try to answer the question "which feature is the canonical way to solve problem X", since from what I understand, the "canonical" way to solve a problem in C++ changes quite frequently as new features are added. It was recommended to me by one of my professors a few years back for being short and accessible to a person who is looking to learn what makes C++ special, not how to program from scratch.
EDIT: Reading the page again, it looks like the book does have some discussion of C++17 and C++20 features, but I focused on the first few chapters which really nail down the whole copy and move semantics feature as well as operator overloading.
When I went on a Modern C++ bender a year or two ago I went with Stroustrup's A Tour of C++ (2nd edition when I bought it, 3rd is out now). It was a good read, covered a lot of details quickly. The rest of my re-learning was using cppreference and reading through the list of standard types and functions/methods on them, trying to translate old (clunky, often subtly broken) C++03 code into modern C++.
"Minimal" and "C++" do not belong in the same sentence :-)
That said, C++ Primer by Lippman, Lajoie, Moo is a good book for beginners. I think the latest edition covers upto C++11. You can then followup with Modern C++ Cookbook by Marius Bancila for newer features upto C++20.
I write low latency C++ for a living. Read "A Tour of C++", focusing on the STL and the basics of the language, basics of templates. Anything else related to STL, look at cppreference.
C hasn't seen a lot of relevant changes since C89 as far as I'm concerned. The most important for me is definitely declare-anywhere (which had been widely supported prior to standardization in C99), it removes most of the friction when quickly trying out new stuff.
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)
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
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
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.
I like it like that. It's also done like that in some codebases that I'm often skimming, e.g. the Linux kernel. I don't worry about the standard in this case. As I've written more and more code, my willingness to do extra bullshit chores when there is no practical reason has been steadily decreasing.
Should you happen to choose an identifier that is already used by the host environment, you'll notice and can fix it. (It has literally never happened to me).
The intention here is that the prefixed variants are "private" i.e. they should not, or only rarely, be used directly. Instead, I use the macro layers to automatically fill in the right values and to improve safety. The above allows me to write
My_Foo *foo = xmalloc(My_Foo);
As you can see, there is very little boilerplate. Prefixing the implementation function makes it less likely to accidentally use them when doing code completion. The prefix nicely documents the relation between the macro and the inline function, and I don't have to bother coming up with a different name or naming scheme.
It's Undefined Behavior by the C standard. So GCC (or other compiler) could use that for optimization, and omit the relevant lines or otherwise mess things up in weird ways. There's no guarantee that you'll notice, and no need for the identifier to be used by the host environment for it to cause a problem.
That said, I'm not aware of any current compiler that does this. It'd be weird.
I'd like to ask HN for references explaining how to layout a large program in C, for someone used to thinking with OOP. For example for a moderately complex arcade game, how would I manage all the states, entities, and interactions? I can write any simple C program but this always trips me up.
Basically just like you would in OOP. Put related variables next to each other in a group and pass this group around. In OOP this is called a class and in classless languages such as C this is called a struct. The functions on this object can be organised in modules just like would be done in OOP. See also https://gamedev.stackexchange.com/a/172405.
Think of the Linux kernel. That's a huge project, organized in an object oriented fashion. Is there a difference between this->method(arg) and method(this, arg)?
It'd be more like typename_method(this, arg, &err) in C because there are no namespaces or exceptions. Also you'll have to manually call destructors.
So yeah, it works fine, just more verbose cause it lacks some syntactic sugar. I prefer C for simple or high-performance programs, and I can see why people might want C++ for larger ones. Problem is some people make a mess pulling out all the stops in C++ OOP features.
define structures, define functions that take those structures as parameters. this is basically what OO is all about. but writing it in C++ will be easier.
Most of the introduction is inaccurate. C++ does its best to reintegrate all new features of C into C++ (not that C has anything much going on since 1999), and a lot of efforts are made to unify the languages. The only reason they're not unified is because there are a few irreductibles that are against it, but as written in the article, even Microsoft is in favour of just having C++ and no C.
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).
To me, the point of it is just to be more explicit and ease maintenance, like `typedef int meters_t`, but more extensible. Compare `int foo_len` to `meters_t foo_len`.
Explicitness: Consider the ambiguousness of `int foo_len` when you're swapping between meters and feet (but use `int` for both).
Ease maintenance: If you want to change the type you use to represent meters (say from `int` to `size_t`, since it might be wise to make it unsigned), compare going through the code and changing each meter-related instance of `int` vs. just changing the `meters_t` struct declaration.
More extensible: Compared to `typedef int meters_t`, using a struct is useful if you ever have to add to the struct. (Maybe a second numeric member representing the conversion to some other unit of length or something.)
For "meters", this doesn't really apply, but using a struct also prevents you from accidentally trying to do math with numeric types that you shouldn't do math with (like ID numbers or something): https://stackoverflow.com/a/18876104/9959012
Where I work, the C++ style guide explicitly says not to alias int types like this cause it becomes annoying for others to keep looking up what each thing really is, and probably YAGNI. I agree with that recommendation.
Any int variable storing meters is probably called "meters" already.
to avoid implicit conversion - you can implicitly convert between meters and ft if its like this:
typedef int feet;
typedef int meters;
this sucks, because they are the same to the compiler. Making them structs makes them unique types, and you can no longer pass a feet value to a meters parameter, etc.
It’s to add minimal type safety. I use this technique in C++ (not with units) even though we have a strong units library. It’s particularly useful for keeping track of things that are scalar-like but get passed around a bunch. Passing around a “badness” in an optimization for example: just a `float` looses its meaning quickly and is hard to track down the comment saying what it means. with `/* Represents the badness of fit to be minimized during optimization… */ struct Badness { float badness; };` you can always find the struct and the documentation next to it. And you can have functions taking and returning them.
I think you don't understand why typedef is used for structs in C and what do struct wrappers have to do with typedefs? It seems to me you don't understand what is being said here.
There is nothing wrong with either practice. The GP is just stating their preferred style.
In fact it is good practice to use struct wrappers for void* pointers to get type safety. On the other hand, a typedef is just for programmer convenience and the compiler doesn't care.
It is largely considered bad and misleading style. Why would you ever hide that a variable is a srruct? Better to keep typedef only for basic types and for function pointers.
Why would I ever care if something is a struct or not a struct? Just that is almost zero useful information.
I do care about the size of a struct sometimes, but that would require me to go to the definition of the struct, so just seeing the word "struct" didn't help me one bit.
And I of course care about the members of the struct, but that again requires me to know what the actual members are which the word "struct" doesn't give me.
So what exactly does omitting the word "struct" hide again?
How is it hidden? Can't the inquiring mind simply examine the codebase to see it's a struct? And if they choose not to, isn't it on them if it turns out to be something other than what they assumed it was without looking?
I think so. Even with a compiled library distributed with a header file but no source, the header file needs to forward declare something like 'typedef struct foo FOO;' So (unless there's some clever trick I've missed) you can always tell it's a struct but not necessarily see the definition.
There is a point, perhaps a little specious, that you put a module's data structures behind a typedef so the interface doesn't change if it changes from a simple data type to a struct. Probably doesn't happen too often.
The perfect C object-oriented-style interface is FILE* from stdio.h. A FILE is a structure full of operating-system-specific file information but you never have to see it or worry about what's in it, you just use the functions.
> There is a point, perhaps a little specious, that you put a module's data structures behind a typedef so the interface doesn't change if it changes from a simple data type to a struct. Probably doesn't happen too often.
you could never do this in C. If it is a "value type" i.e. a non-pointer, then you cannot change the size of the value, without changing the ABI and the function decl.
It is hidden because you have to "examine" somewhere else before you know. But it doesn't matter, because you don't need to know unless it is subject to invisible implicit conversions. What might you do differently, having "examined" the typedef?
I understand the preference for keeping the struct tag, but virtually no other language does this, with modern IDEs it's trivial to find out what the definition of a given type is, so the tag seems superfluous to me in practice.
It's not necessarily bad, but personally I hardly ever do it, and at work the style guide says no. Gets annoying for other readers to keep checking what something really is, and even for the writer it's usually just extra toil that won't really help you.
Like, my variable is already declared as `int meters` probably. I don't need the redundancy of saying `meters_t meters`. Maybe I even want to store meters as a `long` in certain contexts.
>even BIGGER no. This is completely misunderstanding what a typedef is and what it should be used for.
Hard disagree.
Life safer when dealing with things like SI unit types. I used to use lots of suffixes - _km, _m, _seconds, _hours, but I found that to be a lot more noisy (especially derived units and Nth order stuff like acceleration, seconds_per_second, etc) and evidently it would sneak in errors when you started doing calculations and passing them to functions.
Definitely want different types when I have these three representations flowing around in the program:
pressure_bits_t - raw data from the sensor, gets filtered/averaged in this form, then converted to one of these at various stages:
pressure_pascal_x10_t - integer pascal * 10 (i.e fixed point, one decimal)
_t is even a "reserved" suffix in C. Stop using it. If you want compile-time type safety, pick another language like Haskell. A "typedef" is by no means a contract declaration in C.
No it is not reserved, it's just posix's personal style guides.
There's just as much of a name clash possibility when not using _t because lots of other libraries and platforms uses some other convention.
Only ISO C can reserve things, no one else, and ISO C does not reserve the _t suffix.
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:
That is pretty much 100% a bug in the code. Just make it illegal already. D does.For another:
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!