The C23 edition of Modern C
gustedt.wordpress.com394 points by bwidlar 16 hours ago
394 points by bwidlar 16 hours ago
Important reminder just in the Preface :-)
Takeaway #1: "C and C++ are different: don’t mix them, and don’t mix them up"
>Takeaway #1: "C and C++ are different: don’t mix them, and don’t mix them up"
Where "mixing C/C++" is helpful:
- I "mix C in with my C++" projects because "sqlite3.c" and ffmpeg source code is written C. C++ was designed to interoperate with C code. C++ code can seamlessly add #include "sqlite3.h" unchanged.
- For my own code, I take advantage of "C++ being _mostly_ a superset of C" such as using old-style C printf in C++ instead of newer C++ cout.
Where the "C is a totally different language from C++" perspective is helpful:
- knowing that compilers can compile code in "C" or "C++" mode which has ramifications for name mangling which leads to "LINK unresolved symbol" errors.
- knowing that C99 C23 has many exceptions to "C++ is a superset of C" : https://en.wikipedia.org/wiki/Compatibility_of_C_and_C%2B%2B...
The entire I/O streams (where std::cout comes from) feature is garbage, if this was an independent development there is no way that WG21 would have taken it, the reason it's in C++ 98 and thus still here today is that it's Bjarne's baby. The reason not to take it is that it's contradictory to the "Don't use operator overloading for unrelated operations" core idea. Bjarne will insist that "actually" these operators somehow always meant streaming I/O but his evidence is basically the same library feature he's trying to justify. No other language does this, and it's not because they can't it's because it was a bad idea when it was created, it was still a bad idea in 1998, the only difference today is that C++ has a replacement.
The modern fmt-inspired std::print and std::println etc. are much nicer, preserving all the type checking but losing terrible ideas like stored format state, and localisation by default. The biggest problem is that today C++ doesn't have a way to implement this for your own types easily, Barry illustrates a comfortable way this could work in C++ 26 via reflection which on that issue closes the gap with Rust's #[derive(Debug)].
Remember that C++ originally didn't have variadic templates, so something like std::format would have been impossible back in the day. Back in the day, std::iostream was a very neat solution for type safe string formatting. As you conceded, it also makes it very easy to integrate your own types. It was a big improvement over printf(). Historic perspective is everything.
> The biggest problem is that today C++ doesn't have a way to implement this for your own types easily
I’m not sure about the stdlib version, but with fmtlib you can easily implement formatters for your own types. https://fmt.dev/11.0/api/#formatting-user-defined-types
I think the problem is that your idea of "easy" is "Here's a whole bunch of C++ you could write by hand for each type" while the comparison was very literally #[derive(Debug)]. I wasn't abbreviating or referring to something else, that's literally what Rust programmers type to indicate that their type should have the obvious boilerplate implementation for this feature, in most types you're deriving other traits already, so the extra work is literally typing out the word Debug.
> Don't use operator overloading for unrelated operations
This disn't stop with <iostream>, they keep doing it - the latest example I can think of is std::ranges operations being "piped" with |.
Perfectly iostreams happy user since 1993.
int a;
cin >> a;
Then the program goes berserk as soon as the first non-number is read out of standard input. All the other "cin >> integer" lines are immediately skipped.
Yes, I know about error checking, clearing error condition, discarding characters. But it's a whole lot of stuff you need to do after every single "cin>>" line. It makes the simplicity of cin not worth it.
fscanf (STDIN, "%d", &a);
the program goes beserk as soon as the first non-number is read out of standard input.in both cases, you need error checking (which you "know about").
No actual C programmer who has been around the block more than halfway should do that. The mantra is: "read into a character buffer, then parse that".
It's more code, sure, but it buys you a lot of good things. I/O is hard.
How could you ever continue after the second statement without checking if you actually read an integer or not? How would you know what you can do with a?
You couldn't or wouldn't. but why have a read statement like cin>> which looks so nice and clean when you then have to go and check everything with flags and boolean casts on stateful objects.
I agree. It's lunacy. just be explicit and use functions or equivalent like literally every other language.
Well in a language like Haskell you could solve this with monads and do-notation. The general idiom in Haskell is to use a Maybe or Either monad to capture success/failure and you assume you’re on the happy path. Then you put the error handling at the consumer end of the pipeline when you unwrap the Maybe or Either.
I believe Rust has adopted similar idioms. I’ve heard the overall idea referred to as Railway-oriented programming.
In C++ you could implement it with exceptions, though they bring in a bunch of their own baggage that you don’t have to deal with when using monads.
You’re holding it wrong. Like nan, the point is you don’t have to error check every operation.
You check error for the whole batch.
Same, as long as I stay the hell away from locales/facets.
Type safe input/output stream types and memory backed streams served on a silver plate is a pretty decent improvement over C.
Then I suppose you don't care about:
* Performance
* Support for localization (as the format string and positions of values to format differ between languages).
* Code reuse & dogfooding - the data structures used in iostreams are not used elsewhere, and vice-versa
* C and OS interoperability - as you can't wrap a stream around a FILE* / file descritor
* bunch of other stuff...
iostreams work, but are rather crappy.
I care about performance, when it actually matters to acceptance testing.
The less C the merrier.
If you care about correct use of localisation, standard C and C++ libraries aren't really what you're looking for, or even C and C++ to start with.
> If you care about correct use of localisation, standard C and C++ libraries aren't really what you're looking for, or even C and C++ to start with.
What do you recommend instead?C and C++ are the bedrock of operating systems with the best performance and extensive support for all languages.
The only reason why iostreams are slow is because of its incompatible buffering scheme, and the fact that C and C++ need to stay in sync when linked together. And that brand of slow is still faster than other languages, except sometimes those that delegate i/o to pure C implementations.
Historical baggage, they weren't the first system programming languages, got lucky with UNIX's license allowing for widespread adoption, and won't be the last one standing either.
Over the years, I have heard numerous complaints about C++ I/O streams. Is there a better open source replacement? Or do you recommend to use C functions for I/O?
>No other language does this, and it's not because they can't it's because it was a bad idea when it was created, it was still a bad idea in 1998, the only difference today is that C++ has a replacement.
Hindsight is 20/20, remember that. Streams are not that bad of an idea and have been working fine for decades. You haven't named a problem with it other than the fact the operators are used for other stuff in other contexts. But operator overloading is a feature of C++ so most operators, even the comma operator, can be something other than what you expect.
>The biggest problem is that today C++ doesn't have a way to implement this for your own types easily, Barry illustrates a comfortable way this could work in C++ 26 via reflection which on that issue closes the gap with Rust's #[derive(Debug)].
You can trivially implement input and output for your own types with streams.
You appear to be a Rust guy whose motive is to throw shade on C++ for things that are utterly banal and subjective issues.
What they mean is this:
struct Foo {
int a;
float b;
std::string c;
};
Foo foo;
std::cout << foo;
with no extra code. It's called reflection, where the compiler can generate good-enough code to generate a character-stream serialization of an object without any human intervention.C++ can seamlessly include C89 headers.
The C library headers for libraries I write often include C11/C99 stuff that is invalid in C++.
Even when they are in C89, they are often incorrect to include without the include being in an `extern "C"`.
Extern "C" around the prototypes is mandatory, otherwise your linker will search for C++ symbols, which cannot be found in the C libraries you pass it.
Clang supports C11 - 23 in C++, as well as some future C features like fixed-point integers. The main pain points with Clang are just the fundamental differences like void* and char, which don't typically matter much at an interoperability layer.
There's a lot of subtle differences between 'proper' C and the C subset of C++, since C++ uses C++ semantics everywhere, even for its C subset.
Many C++ coders are oblivious to those differences (myself included before I switched from 'mainly C++' to 'mainly C') because they think that the C subset of C++ is compatible with 'proper' C, but any C code that compiles both in a C++ and C compiler is actually also a (heavily outdated) subset of the C language (so for a C coder it takes extra effort to write C++ compatible C code, and it's not great because it's a throwback to the mid-90s, C++ compatible C is potentially less safe and harder to maintain).
For instance in C++ it's illegal to take the address of an 'adhoc-constructed' function argument, like:
sum(&(bla_t){ .a = 1, .b = 2, .c = 3, .d = 4 });
(godbolt: https://www.godbolt.org/z/r7r5rPc6K)Interestingly, Objective-C leaves its C subset alone, so it is always automatically compatible with the latest C features without requiring a new 'ObjC standard'.
Because Objective-C initial proposal is that everything that isn't touched by Smalltalk like code, clearly in brackets or @annotarions, is plain C.
The pre-processor original compiler, before the GCC fork, would leave everything else alone, blindly copying into the generated C file.
Yeah plenty of headers first have `#ifdef __cplusplus` and then they add `extern "C"`. And of course even then they have to avoid doing things unacceptable in C++ such as using "new" as the name of a variable.
It takes a little bit of an effort to make a header work on C and C++. A lot less effort than making a single Python file work with Python 2 and 3.
The '#ifdef __cplusplus extern "C" { }' thing only removes C++ name mangling from exported symbols, it doesn't switch the C++ language into "C mode" (unfortunately).
> C++ code can seamlessly add #include "sqlite3.h" unchanged.
Almost seamlessly. You have to do
extern “C” {
#include "sqlite3.h"
}
(https://isocpp.org/wiki/faq/mixing-c-and-cpp#include-c-hdrs-...)If we're nitpicking then sqlite3.h already has `#ifdef __cplusplus` and `extern "C" {`. So yes, from the user's perspective it is seamless. They do not need to play the `extern "C" {` game.
Yep; I think of it as "C/C++" and not "C" and/or "C++" i.e. one "multi-paradigm" language with different sets of mix-and-match.
My brief foray into microcontroller land has taught me that C and C++ are very much mixed.
It's telling that every compiler toolchain that compiles C++ also compiles C (for some definition of "C"). With compiler flags, GCC extensions, and libraries that are kinda-sorta compatible with both languages, there's no being strict about it.
_My_ code might be strict about it, but what about tinyusb? Eventually you'll have to work with a library that chokes on `--pedantic`, because much (most?) code is not written to a strict C or C++ standard, but is "C/C++" and various extensions.
> because much (most?) code is not written to a strict C or C++ standard, but is "C/C++" and various extensions.
Absolutely true. I generally insist on folks learning C and C++ interoperability before diving in to all the "Modern C or C++" goodness. It helps them in understanding what actually is going on "under the hood" and makes them a better programmer/debugger.
See also the book Advanced C and C++ Compiling by Milan Stevanovic.
Specially relevant to all those folks that insist on "Coding C with a C++ compiler", instead of safer language constructs, and standard library alternatives provided by C++ during the last decades.
Funny because for a long time the Microsoft MSVC team explicitly recommended compiling C code with a C++ compiler because they couldn't be arsed to update their C frontend for over two decades (which thankfully has changed now) ;)
https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-an...
That thing always baffled me, this huge company building a professional IDE couldn't figure out how to ship updates to the C compiler.
> it is hard to say no to you, and I’m sorry to say it. But we have to choose a focus, and our focus is to implement (the standard) and innovate (with extensions like everyone but which we also contribute for potential standardization) in C++.
I mean, yeah if it came from a two member team at a startup, sure focus on C++, understandably. But Microsoft, what happened to "Developers! Developers! Developers!"?
It's not baffling, it's remarkably consistent. They implemented Java as J++ and made their version incompatible in various ways with the standard so it was harder to port your code away from J++ (and later J#). They implemented things in the CSS spec almost exactly opposite the specification to lock people into IE (the dominant browser, if you have to make your site work with 2+ incompatible systems which will you focus on?). Not supporting C effectively with their tools pushed developers towards their C++ implementation, creating more lock-in opportunities.
It was on purpose, Microsoft was done with C, the official message was to move on to C++.
The change of heart was the new management, and the whole Microsoft <3 FOSS.
> It was on purpose, Microsoft was done with C
Indeed, and yet here we are with C23
> The change of heart was the new management, and the whole Microsoft <3 FOSS.
Yeah, agree. To me the turning point was when they created WSL.
Microsoft didn't create C23 and they don't <3 FOSS. They're accepting that they have to deal with FOSS, but installing Windows will still make your Linux system unbootable until you fix it with a rescue disk, among numerous other unfriendly things they do.
I haven't seen Windows fuck up the EFI partition or delete the other entries in a while now. After installing it the machine will usually boot directly into it, but it should be just a toggle in the firmware to switch back to GRUB.
Funnily enough, the intellisense parser does support C syntax because it's using a commercial frontend by edison under the hood. MSVC's frontend doesn't.
Yeah, 12 years ago, when governments couldn't care less about nation state cyberattacks, and Microsoft was yet to be called by the Congress to testify on their failures.
Perfectly valid to do if you need to interface with a large C code base and you just want to do some simple OO here and there. Especially if you cannot have runtime exceptions and the like.
This is how I managed to sneak C++ into an embedded C codebase. We even created some templates for data structures that supported static allocation at compile time.
What would be an example of "simple OO here and there" that cannot be done cleanly in plain C?
Templating on pixel classes so that a blitter builds all supported pixel paths separately and inlines them.
Yes you can do it less cleanly with macros or inline functions. But you can't do it performantly with struct and function pointers.
You can do anything in C that you want to. Of course one can make v-tables and all of that, and even do inheritance.
But having the "class" keyword is nice. Having built in support for member functions is nice.
Sometimes a person just wants the simplicity of C++ 2003.
(In reality I was working on a project where our compiler only supported C++ 2003 and we had a UI library written in C++ 2003 and honestly pure C UI libraries kind of suck compared to just sprinkling in a bit of C++ sugar.)
RAII
Is RAII Object orientation? I thought it was an idiom of C++ by Stroustrup.
It doesn't necessarily have to be OO no. Rust uses RAII and it uses traits instead of traditional OO style inheritance etc. You do need something like destructors/drop trait for it to work as far as I know though.
The killer feature of RAII is when combined with exceptions. But sneaking in exceptions in an embedded C project isn't something I'd encourage or recommend.
C++ imo doesn't offer anything compelling for the embedded usecase. Especially not considering all the footguns and politics it brings.
You can of course be strict and diligent about it but if you are you are pretty much just writing C anyway. Better to do it explicitly.
Allowing the use of the C++ standard library has been one of my biggest regrets (not that it was my decision to make, I fought it).
There are a lot of large C++ shops that purposefully disable exceptions and yet still use RAII usefully. It's so useful that in many C codebases you see people using RAII. For example Gtk has g_autoptr and g_autofree.
One of the most compelling things C++ offers to embedded use case is moving runtime initialization to compile-time initialization by liberally using constexpr functions. You literally ask the compiler to do work that would otherwise be done at runtime.
RAII is useful without exceptions yes. I guess it is the other way around. Exceptions are not useful without RAII (sorry not sorry most garbage collected languages ;)).
But without exceptions it is mostly syntactic sugar anyway.
If compile time initialization is the most compelling usecase I'll rest my case. Good feature, yes! Hardly worth switching language for.
C++ offers lots of compelling things for embedded use cases, like enum classes (finally fixed in C23), constexpr, std::optional, namespaces, and atomics/generics that are much smaller dumpster fires.
There's an effort to extract the good parts and make it work for embedded use cases or even bring them into C. Khalil Estelle on WG21 has been working on an experimental, deterministic runtime for exception handling, to give one example. Constexpr is an example of the latter that's now showing up in C23.
I don't disagree, but these are in the ~2% convenience at most. With the huge baggage of including C++ in a project. The cost of learning C++ easily outweighs all those benefits. If you happen to have a proficient C++ team (that actually know embedded), go for it!
Speaking more broadly than just the std implementation, but result types like optional shouldn't be a 2% convenience, they should be used in most function calls that return errors. Rust is the obvious example here.