> The storage order, the endianness, as given for my machine, is called little-endian. A system that has high-order representation digits first is called big-endian. Both orders are commonly used by modern processor types. Some processors are even able to switch between the two orders on the fly.
Calling big endian "commonly used by modern processor types" when s390x is really the only one left is a bit of a stretch ;D
(Comments about everyone's favorite niche/dead BE architecture in 3… 2… 1…)
The book does say "Both orders are commonly used by modern processor types".
I'd say this sentence is quite misleading, since it would lead you to believe two falsehoods:
1. That both byte orders are equally prevalent in the wild, particularly in systems that are expected to run modern C code.
2. That both byte orders are equally likely to be found in "modern" (new or updated) processor design.
It's not entirely incorrect, but a better phrasing could be used to clarify that little-endian is the more modern and common storage order, but you still cannot ignore big-endian.
The problem about being pedantic is that you can choose different directions to be pedantic in. My "direction" is that code isn't written in a vacuum, it mixes with code millions of other people wrote and runs on machines millions of other people built. As such:
My concern isn't that the phrasing in the book is wrong, and I have expressly not argued that. It's that it presents the issue as having no further depth, and these two choices as equivalent. They aren't. The "Some processors are even able to switch between the two orders on the fly." that follows makes it even worse, at least to me it really sounds like you needn't give any care.
And the people reading this book are probably the people who should be aware of more real-world background on endianness, for the good of the next million of people dealing with what they produced.
MipsBE is very common in edge devices on many networks. You may have 5 MipsBE devices in your home or office without realizing. It's almost never an issue so nobody cares, but they are common.
> Calling big endian "commonly used by modern processor types" when s390x is really the only one left is a bit of a stretch ;D
POWER is bi-endian. In recent versions, Linux on POWER is little-endian (big-endian Linux on POWER used to be popular, until all the distros switched some years back), while AIX and IBM i are big-endian.
AIX and IBM i are probably not quite as alive as IBM mainframes are, but AIX is still arguably more alive than Solaris or HP/UX are, to say nothing of the dozens of other commercial Unix systems that once existed. Likewise, IBM i is just hanging on, yet still much more alive than most competing legacy midrange platforms (e.g. HP MPE which has been officially desupported by the vendor, although you can still get third party support for it.)
True - but at the same time, about half¹ of it is mipsel, i.e. in little-endian mode :). It's also in decline, AFAICS there is very little new silicon development.
Many routers use the MIPS ISA and they can be rooted to get shell access. That's what I did with an old Netgear router, which was like a very low spec SBC. If you have a PS2 lying around, you could try that.
Indeed, if it meant "currently widespread" there'd be a stronger argument for Big Endian with a lot of MIPS and PPC chugging away silently. But interpreting "modern" as recent development, BE is close to gone.
Uh, why so serious? I called it "a bit of a stretch ;D" - there was a reason for that smiley. I'm well aware BE is alive enough to be around.
If you can't live without knowing, sure, my stake in dismissing big endian architectures is that I can't in fact dismiss BE architectures because I have users on it. And it's incredibly painful to test because while my users have such hardware, actually buying a good test platform or CI system is close to impossible. (It ended up being Freescale T4240-QDS devkits off eBay. Not a good sign when the best system you can get is from a company that doesn't exist anymore.)
And at some point it's a question about network protocols/encodings being designed to a "network byte order" determined in the 80s to be big endian. When almost everything is LE, maybe new protocols should just stick with LE as well.
To be fair to IBM, with s390x they do have a "community cloud" programme where open source projects can apply to get a Linux s390x VM to use for things like CI: https://community.ibm.com/zsystems/l1cc/ . But yeah, BE MIPS is super awkward because the target systems are all embedded things that are bad dev/CI machines.
Stupidly enough, "my" software is a routing control plane (FRRouting), so what I need to support are exactly those embedded things. I'm not sure anyone uses FRRouting on a s390x machine. But maybe we should go ask IBM anyway, a BE system is a BE system…
qemu CPU emulation exists too, but that's painfully slow for an actual CI run, and I'm not sure I trust it enough with e.g. AF_NETLINK translation to use the "-user" variant on top of an LE host rather than booting a full Linux (or even BSD).
And in the very best case, proper testing would pit BE and LE systems "against" each other; if I run tests on BE against itself there's a good risk of mis-encodings on send being mis-decoded back on receive and thus not showing as breakage…
… really, it's just a pain to deal with. Even the beauty (in my eyes) of these T4240 ppc64 systems doesn't bridge that :(
Fair—my bad, I can fail at reading tone sometimes.
Would you propose the C abstract machine abstracting away endianness entirely as an alternative? My understanding is that deprecating support for existing architectures is discouraged to every practical extent.
Maybe we failed to communicate because our brains have different endianness? :D
To be honest, I don't think this is a solvable problem. (Changing the C machine concept doesn't do much if you need to process network traffic that uses both (e.g. IP packet [big endian] carrying protobuf [little endian]). It's already mostly a question of data ingress/egress.)
What is solvable though is making sure people are sufficiently aware. And the people who read a book like "Modern C" are probably a very good target audience, building low-level bindings and abstractions. They should know that LE and BE are technically a free-floating design choice, but practically the vast majority of systems is LE now. But at the same time, yes, BE isn't extinct, and won't be any time soon… and it's left to them to make their best possible design given their environments.
Most important aspect of C is its portability. From small microcontrollers to almost any computing platform. I doubt that any new version of C will see that much adoption.
If I want to live on cutting edge I would rather use C++2x or Rust rather than C.
Am I missing something? What benefit this supposedly modern C offers?
One advantage of writing C code is that you don't have annoying discussions about what idiomatic code is supposed to look like, and what language subset is the right one ;)
For the cutting edge I would recommend Zig btw, much less language complexity than both modern C++ and Rust.
One good but less visible side effect of C23 is that it harmonizes more syntax with C++ (like ... = {} vs {0}) which makes it a bit less annoying for us C library maintainers to support the people how want to compile their C code with a C++ compiler.
But it might be a minor problem for STB-style header libraries.
It's not uncommon for C++ projects to include the implementation of an STB-style header into a C++ source file instead of 'isolating' them in a C source file. That's about the only reason why I still support the common C/C++ subset in my C libraries.
Of course they will, just like they did in the past with C11, GNU extensions, or some of the individual features that are now rolled into C23. For example, the 0b notation for binary numbers is widely used in the MCU world.
The microcontroller toolchains are generally built on top of GCC, so they get the features for free. There are some proprietary C compilers that are chronically lagging behind, but they are not nearly as important as they used to be two decades ago.
Most devices that are 6+ years old (as far as I can tell) use C99. If not C89. And/or C++17, if that.
That's A LOT of devices out there. A lot of which still get maintenance and even get feature updates (I'm working on one right now, C99).
So the claim that "C codebases generally use C11 or C17, and C++ code bases use C++20" intuitively sounds like totally untrue to someone working in embedded C/C++. I've been doing this for 15+ years and I've never touched anything higher than C99 or C++17.
If you're talking about gaming, sure. But that's not "C code bases generally".
The `thread_local` specifier is used on a few microcontroller platforms already, but would be absolutely illegal in C11 and before to use. However, it vastly simplifies memory management in a threaded context.
Why would I rather step into the world of C++ just to deal with that?
Won't any llvm/gcc supported target get the new version of C automatically? You won't get it in the vendor-modified ancient gcc toolchain for some other arch though.
There are many embedded platforms that do not gcc/llvm based compilers.
Also most companies making those platforms are not good at updating their toolchains. Expecting developers to compile their own toolchain, that is unsupported by platform vendor, is too much to ask.
Also GCC dropped support for certain architectures along the way, and even if you are willing to compile your own toolchain, it may not work for you.
I'm with you on this. The feature list reads like a subset of later C++ standards that fit within C's (deliberately) rudimentary feature set.
You could, in theory, just use C++ and be done with it. But like any C++ project you'd need a pretty strict style guide or even a linter, but this time it would have to be extra restrictive lest you slide into full C++ territory. And maybe that's a major stumbling block for some people?
Personally this[1] just makes C much more complicated for me, and I choose C when I want simplicity. If I want complicated, I would just pick C++ which I typically would never want. I would just pick Go (or Elixir if I want a server).
"_BitInt(N)" is also ugly, reminds me of "_Bool" which is thankfully "bool" now.
[1] guard, defer, auto, constexpr, nullptr (what is wrong with NULL?), etc. On top of that "constexpr" and "nullptr" just reeks of C++.
That said, Modern C is an incredible book, I have been using it for C99 (which I intend to continue sticking to).
One of the few advantages of ISO standardization is you can just read the associated papers to answer questions like this: https://wg21.link/p2312
The quick bullet points:
* Surprises when invoking a type-generic macro with a NULL argument.
* Conditional expressions such as (1 ? 0 : NULL) and (1 ? 1 : NULL) have different status depending how NULL is defined
* A NULL argument that is passed to a va_arg function that expects a pointer can have severe consequences. On many architectures nowadays int and void* have different size, and so if NULL is just 0, a wrongly sized argument is passed to the function.
> If I want complicated, I would just pick C++ which I typically would never want
In my opinion, complexity doesn't scale linearly like this. Sometimes, in fact often times, having more complex tools means a simpler process and end result.
It's like building a house. A hammer and screwdriver are very simple. A crane is extremely complex. But which simplifies building a house? A crane. If I wanted to build a house with only a hammer and screwdriver, I would have to devise incredibly complex processes to get it done.
You see the same type of thing in programming languages. Making a generic container in C++ is trivial. It's very, very hard in C. You can make it kind of generic. You can use void * and do a bunch of manual casting. But it's cumbersome, error prone, and the code is more complex. It's counter-intuitive - how can C, a simpler language, produce code that is more complex than C++?
Or look at std::sort vs qsort. The power of templates and functors makes the implementation much simpler - and faster! We don't have to pass around void * and dereference them at runtime, instead we can build in comparison into the definition of the function itself. No redirection, no passing on the stack, and we can even go so far as to inline the comparison function.
There's really lots of examples of this kind of stuff. Point being, language complexity does not imply implementation complexity.
In my experience, complex tools encourage fluffy programming. You mention a generic container; if I were using C, I just wouldn't use a generic container; instead, I'd specify a few container types that handle what needs handled. If there seem to be too many types, then I immediately start thinking that I'm going down a bad architecture path, using too many, or too mixed, abstraction layers, or that I haven't broken down the problem correctly or fully.
The constraints of the tool are inherited in the program; if the constraints encourage better design, then the program will have a better design. You benefit from the language providing a path of least resistance that forces intentionality. That intentionality makes the code easier to reason about, and less likely to contain bugs.
You do pay for this by writing more boilerplate, and by occasionally having to do some dirty things with void pointers; but these will be the exception to the rule, and you'll focus on them more since they are so odd.
Sometimes, but I would argue that C is too simplistic and is missing various common-sense tools. It's definitely improving, but with things like namespaces there's pretty much no risk of "too complex" stuff.
Also, I wouldn't be saying this if people didn't constantly try to recreate C++-isms in C. Which sometimes you need to do. So, then you have this strange amalgamation that kind of works but is super error prone and manual.
I also don't necessarily agree that C's constraints encourage better design. The design pushes far too much to runtime, which is poor design from a reasoning point of view. It's very difficult to reason about code when even simple data models require too much indirection. Also, the severely gimped type system means that you can do things you shouldn't be able to do. You can't properly encode type constraints into your types, so you then have to do more validation at runtime. This is also slightly improving, starting with _Bool years ago.
C++ definitely is a very flawed language with so, so many holes in its design. But the systems it has in place allows the programmer to more focus on the logic and design of their programs, and less on just trying to represent what they want to represent. And templates, as annoying as the errors are, prevent A LOT of runtime errors. Remember, every time you see a template that translates into pointers and runtime checks in C.
I think that is fair. A simple language with a simple memory model is nice to work with.
I also think that it wouldn't be bad for code to be more generic. It is somewhat unnecessary for a procedure to allow an argument of type A but not of type B if the types A and B share all the commonalities necessitated by the procedure. Of course procedures with equivalent source code generate different machine code for different types A or B, but not in a way that matters much.
I believe it is beneficial for the language to see code as the description of a procedure, and to permit this description to be reused as much as possible, for the widest variety of types possible. The lack of this ability I think might be the biggest criticism I have for C from a modern standpoint.
I feel that if C had tagged unions and a little sugar you could write non magical generic functions in C. Non magical meaning unlike C++ etc instead of the compiler selecting the correct function based on the arguments the function itself can tell and handle each case.
Basically you can write a function that takes a tagged union and the compiler will passed the correct union based on named arguments.
int ret = foo(.slice = name);
int ret = foo(.src = str, .sz = strlen(str));
auto is mostly useful when tinkering with type-generic macros, but shouldn't be used in regular code (e.g. please no 'almost always auto' madness like it was popular in the C++ world for a little while). Unfortunately there are also slight differences between compilers (IIRC Clang implements a C++ style auto, while GCC implements a C style auto, which has subtle differences for 'auto pointers' - not sure if those differences have been fixed in the meantime).
_BitInt(N) isn't typically used directly but typedef'ed to the width you need, e.g.
typedef _BitInt(2) u2;
The 'ugly' _B syntax is needed because the combination of underscore followed by a capital letter is reserved in the C standard to avoid collisions with existing code for every little thing added to the language (same reason why it was called _Bool).
AFAIK defer didn't actually make it into C23?
I'm also more on the conservative side when it comes to adding features to the C standard, but IMHO each of the C23 additions makes sense.
> IIRC Clang implements a C++ style auto, while GCC implements a C style auto, which has subtle differences for 'auto pointers' - not sure if those differences have been fixed in the meantime
Both have compatibly implemented the standard C++ auto. Since 2011 or so.
IIRC Clang implements 'C++ semantics' for C23 auto, while GCC doesn't.
Last time I brought that up it turned out that both behaviours are 'standard compliant', because the C23 standard explicitly allows such differing behaviour (it basically standardized the status quo even if different compilers disagreed about auto semantics in C).
The old definition did not even specify wether it was a pointer or an integer. So for platforms that did not follow the Posix ((void*)0) requirement it was a foot gun that had neither the type nor the size of a pointer.
> On top of that "constexpr" and "nullptr" just reeks of C++.
Probably because they where back ported from C++. You can still use NULL, since that was apparently redefined to be nullptr.
You are right. Hereby I correct my parent comment: I talked about my own personal experience[1], but yeah, as you said, stddef.h is often required (and yes, often I do not need stdio.h, stddef.h is what I need) which defines NULL, which was my point. If it is often required, then it does not matter whether you have to include a header file or not, IMO.
Just include the stddef.h header if you want to use NULL, similarly to how you include a header file if you want to use anything else, e.g. bool from stdbool.h.
[1] I am not entirely sure in retrospect, actually, as I might be misremembering, but my point stands with or without stdio.h!
I like Modern C. I have reviewed it favorably in several places. I agree it is intermediate.
I think 21st Century C by Ben Klemens and C Programming a Modern Approach by King are both more approachable alternatives as a modern C companions to K&R.
Note that this is not a complete list, fwiw. For example, I doesn't include "Effective C." [1].
I like "Effective C" over "Modern C" because it's more engaging ... "Modern C" is super rigorous and feels a bit like reading an annotated spec of the language, which is what an expert may need, but makes for a dull read for a casual C user like me.
I've been using modern C++ for a personal project (a language interpreter) for the last year+. I constantly think of switching to C, because of the mental burdens of C++, and because of the problems with tooling (Visual Studio's IntelliSense still barely works, because I use C++20 modules), and compile times get ugly because of the way the language failures force so much into interfaces (even with modules). But on the flip side I've gotten so used to classes, member functions, generic programming (templates), namespaces... I may be hooked.
Yeah, I did. I want something low level and cross platform, including mobile. I think when I tried the C# for iOS stuff, nothing worked. But it's probably too much VM/runtime for me anyway, for this project.
iOS C# is more or less fine, there is quite a bit of work done in .NET to make this better still. .NET 9 gains native Swift Library Evolution ABI support even - you can literally declare DllImports against public Swift APIs by simply annotating them with [typeof(CallConvSwift)], it's not as convenient as it sounds but it's only a matter of time when the tools like https://github.com/royalapplications/beyondnet adopt this. It's going to get much better once MonoAOT is replaced with NativeAOT for all major publish modes for iOS.
It's only been a few years since I've come to feel I can rely on C compilers all supporting C99, for a library I'm maintaing [1]. And after a couple of years, sure enough - I get an issue opened asking for C89 compatibility because of some arcane embedded toolchain or what-not.
So, C23? ... that's nice and all, but, let's talk about it in 20 years or so T_T
Can someone link me to an article that explains why C is basically frozen at C99 for all practical purposes? Few projects worth talking about leverage features from C11 and newer
C99 is still new! Microsoft tried to kill C by refusing to implement anything that wasn't also in C++. MSVC was 16 years late implementing C99, and implemented only the bare minimum. Their C11 implementation is only 11 years late.
I suspect that decades of C being effectively frozen have caused the userbase to self-select to people who like C exactly the way it is (was), and don't mind supporting ancient junk compilers.
Everyone who lost patience, or wanted a 21st century language, has left for C++/Rust/Zig or something else.
Most of us liking a good language just did not use MSVC. I do not think many people who appreciate C's simplicity and stability would be happy with C++ / Rust. Zig is beautiful, but still limited in many ways and I would not use it outside of fun projects.
I don't even use Windows, but I need to write portable libraries. Unfortunately, MSVC does strongly influence the baseline, and it's not my decision if I want to be interoperable with other projects.
In my experience, Windows devs don't like being told to use a different toolchain. They may have projects tied to Visual Studio, dependencies that are MSVC-only or code written for quirks of MSVC's libc/CRT, or want unique MSVC build features.
I found it hard to convince people that C isn't just C (probably because C89 has been around forever, and many serious projects still target it). I look like an asshole when I demand them to switch to whole another toolchain, instead of me adding a few #ifdefs and macro hacks for some rare nice thing in C.
Honestly, paradoxically it's been easier to tell people to build Rust code instead (it has MSVC-compatible output with almost zero setup needed).
Microsoft basically sabotaged C99 by not implementing any of its features until around 2015 in the Visual Studio C compiler, and then still took until 2019 before they acknowledged their failure and started supporting more recent C versions again (MSVC is still reliably behind Clang and GCC when it comes to their C frontend though).
And back around 2010 MSVC still mattered a lot (which sounds weird from today's pov where most developers appear to have moved to Linux).
But OTH, few projects actually need C11 features (and C11 actually took one thing away from C99: VLAs - nothing of value was lost though).
C23 might be the first version since C99 that's actually worth upgrading to for many C code bases.
C11 gave us one very important thing: a standardized memory model! Just like in C++11, you can finally write cross-platform multithreaded code with standardized atomics, synchronization primitives and threads. Unfortunately, compiler/library support is still lacking...
Wow, the use of attributes like [[__unsequenced__]], [[maybe_unused]] and [[noreturn]] throughout the book is really awful. It seems pretty pedantic of the author to litter all the code examples with something that is mostly optional. For a second I wondered if C23 required them.
This is not how C standards work. If it appears in the standard, it means that it is already implemented in some compilers (in that case, at least in gcc and clang).
It's a nuisance to implement the thing you want to add to the standard yourself. It's easier to ship it in the language and then complain at compiler devs that they're running behind the edge of progress.
This interacts in the obvious way with refusing to correct mistakes after the fact for fear of breaking user code.
I don't believe anyone has written a paper along the lines of "let's not bother with the existing practice part anymore", it's more an emergent feature of people following local incentive structures.
I've heard something along the lines of "the standard is to define facilities that will be used in most programs, and to codify widespread existing practice." That was in the context of "I don't like this proposed feature," though. This was for C++, not C.
A lot of stuff in the C++11 standard library was based on widespread use of Boost. Since then, I don't know. Also, were things like templates and lambdas implemented as compiler extensions before standardization? I don't know, but I doubt it. Maybe "we're a committee of people who will decide on a thing and we hope you like it" was always the norm in many ways.
Right. Also it might it sound like array-to-pointer decay is forced onto the programmer. Instead, you can take the address of an array just fine without letting it decay. The type then preserves the length.
int main() {
int a[3];
return foo(a);
}
> gcc test.c
> ./a.out
Oops.
D:
int foo(int[] a) { return a[5]; }
int main() {
int[3] a;
return foo(a);
}
> ./cc array.d
> ./array
core.exception.ArrayIndexError@array.d(1): index [5] is out of bounds for array of length 3
You need to take the address of the array instead of letting it decay and then size is encoded in the type:
int foo(int (*a)[6]) { return a[5]; }
int main() {
int a[3];
return foo(&a);
}
Or for run-time length:
int foo(int n, int (*a)[n]) { return (\*a)[5]; }
int main() {
int a[3];
return foo(ARRAY_SIZE(a), &a);
}
/app/example.c:4:38: runtime error: index 5 out of bounds for
type 'int[n]'
My guess is that it was intended to escape the * since unescaped * in regular text on HN results in italics. Since the text in question is in a code block, though, that escaping is not needed.
Which is not trivial to parse, bing posited the answer as authoritative, and if you look at the code it is really nothing, it seems to be a folklore concept, and as such, it is much more aptly transmitted by speaking to a human and getting a live version than by googling an authoratitative static answer.
The thing is though that even with array bounds checking built into the language, out of bounds access due to programming error can still be attempted. Only this time it's safer because an attacker can't use the bug (which still exists) to access memory outside of bounds. In any case, the program still doesn't work as intended (has bugs) because the programmer has attempted, or allowed the attempt, to access out of bounds memory.
Writing safe code is better than depending on safety features. Writing safe code is possible in any programming language, the only things required are good design principles and discipline (i.e. solid engineering).
That's implementation defined behavior, not undefined behavior. Undefined behavior explicitly refers to something the compiler does not provide a definition for, including "safe defaults."
>Possible undefined behavior ranges from ignoring the situation completely with unpredictable results ... or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message)
So a compiler is absolutely welcome to make undefined behavior safe. In fact every compiler I know of, such as GCC, clang, MSVC has flags to make various undefined behavior safe, such as signed integer overflow, type punning, casting function pointers to void pointers.
The Linux kernel is notorious for leveraging undefined behavior in C for which GCC guarantees specific and well defined behavior.
It looks like there is also the notion of unspecified behavior, which gives compilers a choice about the behavior and does not require compilers to document that choice or even choose consistently.
And finally there is what you bring up, which is implementation defined behavior which is defined as a subset of unspecified behavior in which compilers must document the choice.
Something can be UB according to the standard, but defined (and safe) according to a particular implementation. Lots of stuff is UB according to the C or C++ standard but does something sensible in gcc and/or clang.
This distinction does not exist in K&R 2/e which documents ANSI C aka C89, but maybe it was added in a later version of the language (or didn't make it into the book)? According to K&R, all overflow is undefined.
I don't have my copy of K&R handy, but this distinction has existed since the initial codification. From C89:
3.1.2.5 Types
[...] A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting unsigned integer type.
Yes, as unsigned overflow is fine by default. AFAIK the issue was originally that there were still machines that used ones complement for describing negative integers instead of the now customary twos complement.
Except for C99 which added designated init and compound literals. With those it almost feels like a new language compared to C89 (and the C99 designated init feature is so well thought out that it still beats most similar initialization patterns in more recent languages, including C++, Rust and Zig - only Odin seems to "get it").
Ofc this has always been an option. In my C heyday I used to put a space on both sides of the star. It makes for a more consistent syntax when you have multi layer pointers with const at various layers. For example:
// mutable pointer to mutable data:
char * str;
// Immutable pointer to immutable data:
char const*const str;
// Mutable pointer to an immutable pointer to a mutable pointer to immutable data:
Given that putting it on the right reflects the actual syntax tree of the code, why do you find it "horribly confusing"?
I mean, one can reasonably argue that C & C++ declarator syntax is itself horribly confusing because it doesn't read left-to-right. But it is what it is, so why pretend that it's something else?
>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.
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.
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.
>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.
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.
I know what reflection is of course. C++ makes it easy to implement IO. If you're asking for a reflection-based solution with less effort, you are practically asking for zero extra code. Anyway, C++ does not yet have reflection but who's to say how anyone wants any particular data to be dumped? A default implementation is nice but less useful than you make it sound. In any case, there are libraries approximating what you described (usually with macros and stuff) and reflection is totally coming at some point.
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.
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.
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.
Considering that they are evolving, I think they are more likely than not to stay standing. There might be other similar languages developed in parallel, but after over 30 years of whining C and C++ are still popular. I don't expect that to change.
It's like using a sledgehammer for a picture hook. I think QT is a great tool, and it solves this problem nicely, but it's a full blown framework, and if you're already using something else - particularly if it's something "light" like SDL, or just a platform specific library like Win32, it's an awful lot to pull in (plus compile times, licensing, etc).
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?
I did not know this nor indeed that Sather existed. Thanks. I don't feel as though "But Sather did it too" counts as a good reason for I/O streams, but thanks for telling me.
I don't think it's a good reason, and FWIW I'm pretty sure they got the idea from C++ - iostream design predates ISO C++ by quite a bit. I remember seeing << for that with stuff like Borland C++ 3.1 that also originates in 90s, back when you had to write #include <iostream.h> etc. Just noting that this is not such an obviously bad idea that nobody else hasn't fallen into the same trap.
> 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.
I think this is a very lazy and somewhat conspiratorial take.
C++'s IO stream library, along with C++'s adoption of std::string, is a response to and improvement over C's standard library support for IO. That alone makes it an invaluable improvement. It's easy and very lazy to look back 30 years ago and badmouth things done back then.
It's also easy to complain about no one proposing changes when literally anyone, including you, can propose changes. The only need to do the legwork and put their money where their mouth is. The funny part is that we see frameworks putting together their own IO infrastructure and it ends up being not good, such as Qt's take on IO.
But talk is cheap and badmouthing doesn't require a pull request.
The problem is precisely that C++ iostream library was, in practice, not an improvement on C stdio in many ways. Some of us were actually there 30 years ago, and even right after C++98 was standardized, it was pretty common for (then-)modern C++ projects to adopt all of stdlib except for iostreams (and locales/facets, another horrible wart).
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:
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'.
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).
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.
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) ;)
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.
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.
> installing Windows will still make your Linux system unbootable until you fix it with a rescue disk
This is no longer true. On UEFI systems the only thing you have to do normally is fix the boot order. In fact installing Linux first and Windows second tends to be the better dual-boot strategy nowadays.
Fixing the boot order can be done from UEFI setup, and even from Windows command line
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.
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.)
You can obviously build any runtime system you desire in C, including one that parses and executes C code with additional features added in. The wisdom of doing this is questionable.
Though I've actually seen macro systems that do things akin to destructors, although less automatically.
You can handcode vtables in C, just as you can handcode loops in assembly (i.e. it works but it's verbose, not particularly readable, and brings more footguns).
But why would you do that if you have an instrument that lets you work at the same level as C, but with methods provided as a proper abstraction that maps exactly to what you'd have written yourself anyway?
I don't know, I never found the "proper abstraction" be more than irrelevant syntactic sugar. And the cost of C++ is that you end up putting everything in the header (IMHO the biggest design flaw of the language) and then compile time start to get long....
At the very least, the proper abstraction is the one that guarantees that you pass the same pointer as the first argument of the method call as the one you used to read the vtable from.
And no, you don't have to put everything in the header with C++, and especially not if you're using it in "C with classes" mode. C++ only needs it for templates - and even then only if you want to use implicit instantiation, with `extern template` available for fine-grained control allowing you to have those specializations separately compiled and implementations kept out of the header file.
On a high level, "object orientation" means you think of your code as representing the state and interactions of objects. You can equally well do this in assembly. If you think of some namespace as a "singleton object" then that's what it is.
I guess what you're really asking is what are the best or most common ways to do OO in C?
Oh. I learned that object orientation is primarily a way to structure data and code, such that the data is encapsulated with the code that works on it, in so called objects. So an Object is the Data, plus the functions that work on the data, an ensure that some invariants are kept. In OO parlance, that code gets executed by sending messages (calling methods).
Where can I find something about objects being "think of your code as representing the state and interactions of objects" honesty totally new to me.
So no, certainly I'm not asking ways to do OO in C. But it seems to be more definitions of object orientation as I thought...
There's no clear definition of what OO is, so the best you can do pragmatically is look at mainstream languages that are broadly recognized as OO and try to deduce the commonalities.
If you do that, you'll notice that, for example, encapsulation is not a part of that de facto definition, because languages like Python and (until recently) JavaScript lack it, despite being considered OO.
Indeed, the only two things that appear to be consistently present in all OO languages are: 1) some notion of object identity as distinct from object state, and 2) runtime polymorphic dispatch.
> Where can I find something about objects being "think of your code as representing the state and interactions of objects" honesty totally new to me.
I’m scratching my head how you think this is materially different than what you described in your first para. s/state/data and s/interactions/methods.
If anything though I would say the GP is more aligned with the classic definition as it highlights the focus is more on the messages (interactions) themselves rather than the implementation.
Correct, and you did ask specifically for OO things, but I thought I'd list namespaces too as far as “C++ things you might use when writing C-like C++ code”.
Another big one that I always forget C still doesn't support is function overloading.
Function overloading is a feature that makes code less self-documenting without providing any meaningful value. Operator overloading is more interesting, because you can build you domain language with nice syntax. But I also tend to think that this is not really worth it.
Function overloading enables the building of powerful idioms like swap, operator== and this [1] proposal for composable hashing to name a few. And when combined with templates (and ADL to provide some encapsulation with namespaces) you can build some interesting abstractions akin to the io module in go, like this implementation of io.ReadFull():
template<typename R>
constexpr ssize_t
io::ReadFull(R reader, char *buf, size_t len)
{
ssize_t recvd = 0, ret;
do {
ret = read(reader, &buf[recvd], len - recvd);
if (ret > 0)
recvd += ret;
else
break;
} while (recvd < len);
return recvd;
}
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.
Strong disagree on that one. Even though C++ has a lot of features that take a while to learn, getting started with C++ is simpler by miles than getting started with Rust.
One exception to that in my experience: dependencies, although I think that's a bit deceiving as yes, it's easier to get dependencies in Rust but in some areas they're way less mature and can sometimes be a pain to work with (usually when dealing with big C or C++ libraries that have been quickly glued to a Rust interface to be available in Rust).
Agree, writing bad C++ is easy. But being competent in C++ requires much more than being competent in rust.
And not being competent in C++ is not great. You are going to be much more productive in C. The feedback loop is much faster, that is, feedback from your designs.
Contrast with Rust which is harder to get going but doesn't require nearly as much to be decent.
A couple of months ago, in the company I work, there was a talk from HR, where they explained how to make a good CV (the company is firing lots of people). She say: "if you have experience in programming C, you can writing just that, or, if you have lots of experience in C, is customary to write ``C++ Experience'' "
Sooo... yeah... I should definitely change company!
can't believe so many people are arguing against this honestly. you don't mix them in the sense the author means. I take it these people didn't read the paragraphs this was the 'takeaway' from.
For example, the primary reason for the sentence seems to be from the text:
"Many code examples in this book won't even compile on a c++ compiler, So we should not mix sources of both languages".
It's not at all about the ability to use c libraries in c++ projects or vice versa :S.... c'mon guys!
It should be ++C because with C++ the value you get from the expression is the old one.
If you're asking why people use pre-increment by default instead of post-increment, it's mostly historical. The early C compilers on resource-constrained platforms such as early DOS were not good at optimization; on those, pre-increment would be reliably translated to a simple ADD or INC, whereas code for post-increment might generate an extra copy even if it wasn't actually used.
For C++ this was even worse with iterators, because now it depended on the compiler's ability to inline its implementation of postfix ++, and then prove that all the copies produced by that implementation have no side effects to optimize it to the same degree as prefix ++ could. Depending on the type of the underlying value, this may not even be possible in general.
The other reason is that all other unary operators in C are prefix rather than postfix, and mixing unary prefix with unary postfix in a single expression produces code that is easy to misunderstand. E.g. *p++ is *(p++), not (*p)++, even though the latter feels more natural, reading it left-to-right as usual. OTOH *++p vs ++*p is unambiguous.
K&R seems to use pre-increment early on, then post-increment consistently (or a lot, anyway, I haven't done a thorough check) after chapter 3, in situations where either would do. In fact, after introducing post-increment at 2.8.
The PDP-11 that C originally targeted had address modes to support the stack. Pre-increment and post-decrement therefore did not require a separate instruction; they were free. After the PDP-11 went the way of the dodo, both forms took a machine cycle so it (mostly) became a stylistic issue. (The two operators have different semantics, but the trend to avoid side-effects in expressions means that both are most often used in a single expression statement like "++x;" or "x++;", so it comes down to your preferred style.)
memcpy etc also take the destination as their first argument, and it mirrors the usual way we write assignments. Personally I always found Intel syntax to be more straightforward, but I think it's ultimately down to whatever one was exposed to first.
I wonder sometimes why we keep insisting on the "OP ARG, ARG" format in general for assembly. Why not something like `MOV X -> Y` that would make it absolutely clear and unambiguous? For that matter, why not `COPY X -> Y`, since that's what it actually does?
I rarely use pre-increment tbh, but post-increment all the time for array indices (since typically the array should be indexed with the value before the increment happens).
If the pre- or post-increment behaviour isn't actually needed, I prefer `x += 1` though.
People seem to mostly write a typical for loop ending with ; ++i){
But I write ; i++){ and seeing it the other way round throws me off for a minute, because I think, as you put it, why would you use those very particular semantics?
In C++ the semantics can differ, in that copying an object for post-increment might require a memory allocation internally (for example in the case of a BigInt class), which may fail and throw an exception. For consistency, using pre-increment by default and unless you really need post-increment, is a good habit.
> why would you use those very particular semantics?
The difference is that i++ has to keep a copy to the original around as the return value is the pre-increment value, while with ++i that isn't needed as the resulting value is being returned.
In the for loop that shouldn't matter as a) for an integer it is essentially for free (it is just reordering when the relevant register is set) and b) that value is hopefully optimized out anyways by the compiler, however as there are cases where it matters some people prefer the ++i style, some just think it looks better.
I've seen either style, but it the argument about which is proper is pointless. Any modern compiler will optimize either equally well, unless you're doing something that actually depends on the order of the increment.
No, for fundamental datatypes pre/post-increment doesn't matter, but for classes that overload those operators, the postfix form creates a temporary object hence people write
Why use this operator? Like most C and C++ features the main reason tends to be showing off, you learned a thing (in this case that there are four extra operators here) and so you show off by using it even if it doesn't make the software easier to understand.
This is not one of those beginner -> journeyman -> expert cycles where coincidentally the way you wrote it as a beginner is identical to how an expert writes it but for a very different reason. I'd expect experts are very comfortable writing either { x = k; k += 1; } or { k += 1; x = k; } depending on which they meant and don't feel an itch to re-write these as { x = k++; } and { x = ++k; } respectively.
I'm slightly surprised none of the joke languages add equally frivolous operators. a%% to set a to the remainder after dividing a by 10, or b** to set b as two to the power b or some other silliness.
Note that in your example there appear to be three distinct meanings:
1. prefix incr/decr precedence: "stack[--sp]"
2. postfix incr/decr precedence: "s[j++]"
3. i have no particular preference for the precedence and am just using a shorthand I inherited from my ancestors whose use cases are no longer relevant to me: "i++" in your for loop
My rank speculation is that C programmers get in a habit of #3 and then forget to consider precedence in an expression where it matters.
In any case, it would be interesting to do a scan of github to see how often prefix and suffix incr/decr had to get switched up in a bugfix patch.
Kernighan spends a page whittling strcpy down to just that, with various intermediate versions. After showing you that version, he describes it like this:
Although this may seem cryptic at first sight, the notational convenience is considerable, and the idiom should be mastered, because you will see it frequently in C programs.
I payed for this on manning and they didn’t even release the final version yet. I guess I didn’t understand what I was buying, but I can’t help feel a bit cheated.
Continuing to use a memory-unsafe language that has no recourse for safety and is full of footguns and is frankly irresponsible for the software profession. God help us all.
By the way, the US government did the profession no favors by including C++ as a memory-unsafe language. It is possible to write memory-safe C++, safe array dereferencing C++. But it’s not obvious how to do it. Herb Sutter is working on it with CppFront. The point stands that C++ can be memory-safe code. If you make a mistake, you might write some unsafe code in C++. But you can fix that mistake and learn to avoid it.
When you write C, you are in the bad luck shitter. You have no choice. You will write memory—unsafe code and hope you don’t fuck it up. You will hope that a refactor of your code doesn’t fuck it up.
Ah, C, so simple! You, only you, are responsible for handling memory safely. Don’t fuck it up, cadet. (Don’t leave it all to computers like a C++ developer would.)
You can't just put a language in the bin that has been used for 50 years and that a huge percentage the present day software infrastructure is built on.
I see comments like yours everywhere all the time and I seriously think you have a very unhealthy emotional relationship with this topic. You should not have that much hate in your heart for a programming language that has served us very well for many decades and still continues to do so. Even if C was literally all bad (which imho isn't even possible), you shouldn't be that angry at it.
When you write C++, you can allocate memory all day long and write ZERO delete statements. That is possible, I’ve been writing C++ like that since 1998 (Visual C++ 5.0 and lcc). Can you imagine allocating memory and never risk a premature or a forgotten delete? It is not possible in C. You can call it opinion, but I see fact. That makes C all that bad.
When I say put it in the bin, I don’t mean that good software hasn’t been written already with it, or can’t be written with it. But you should stop using it given the earliest opportunity. When given the ability to write object-oriented software, clever engineers with too much time add insane complexity justified by unproven hypotheticals. Believe me, I know very well why people shy away from C++ like a trauma response. Overly-engineered/overly-abstracted complexity, incomprehensible template syntax, inadequate standard library, indecipherable error messages, C++ has its warts. But it is possible to write memory-safe software in C++, and it is not in C (unless we are talking about little code toys!). My answer is that you don’t have to write complicated garbage in C++. Keep it simple like you are writing C. Add C++ features only to get safety. Add polymorphism only when it solves a problem. Never write an abstract class ahead of time. Never write a class ahead of time.
Downvote me all day long. Call me angry. When billions of dollars are lost because someone, in our modern age, decided to write new software in C, or continue to develop software in C instead of switching to a mixed C++/C codebase with an intent to phase out new development in C.
It’s hard not to get angry when modern software is written with avoidable CVEs in 2020’s. Use after free, buffer overflows, are you kidding me? These problems should have been relics in 2010+, but here we are.
Programmers make stupid mistakes in the safest languages too, even more so today when software is a career and not a hobby. What does it matter if the memory allocation is safe when the programmer exposes all user sessions to the internet because reading Dockers' documentation is too much work? Even Github did a variant of this with all their resources.
Because memory vulnerabilities don't make programs immune to other dumb mistakes. You get these vulnerabilities on top of everything else that can go wrong in a program.
Manual checking of memory management correctness takes extra time and effort to review, debug, instrument, fuzz, etc. things that the compiler could be checking automatically and reliably. This misplaced effort wastes resources and takes focus away from dealing with all the other problems.
There's also a common line of thinking that that because working in C is hard, C programmers must be smarter and more diligent, so they wouldn't make dumb mistakes like the easy-language programmers do. I don't like such elitist view, but even if true, the better programmers can allocate their smarts to something more productive than expertise in programs corrupting themselves.
Because memory vulnerabilities don't make programs immune to other dumb mistakes. You get these vulnerabilities on top of everything else that can go wrong in a program.
The issue is that these great new tools don't just fix the old vulnerabilities, they also provide a lot of new, powerful footguns for people to play with. They're shipping 2000 feet of rope with every language when all we need is 6 feet to hang ourselves.
There has been a bunch of failed C killers, and C++ has massively shat the bed, so I understand that people are jaded.
However, this pessimistic tradeoff is just not true in case of Rust — it has been focused from the start on preventing footguns, and actually does a great job of it. You don't trade one kind of failure for another, you replace them with compilation errors, and they've even invested a lot of effort into making these errors clear and useful.
I am worried where "official" C is going. Its syntax which is already too complex and already does too much, but that would require to "break" backward compatibility namely it would require "porting". But since it would be still "C" that amount of work should be close to "a bit" of "step by step" refactoring
For instance, only sized types:u8...s64, f32, f64... no implicit casts except for void* and literals, no integer promotion, no switch, no enum, only one loop keyword (loop{}!), no anonymous code block, and no toxic attribute like "packed structure" which makes us lose sight of data alignment... no _generic, typeof, restrict, syntax based tls, etc...
But we would need explicit atomics, explicit memory barriers, explicit unaligned memory access.
Instead of adding and complexifying C to make writing a naive compiler more and more complex, long and a mouse and cat catchup "to the standard" tedious task, what should be done is exactly the other way around.
In end, I don't trust C officials anymore, I tend to stick to C99, or even assembly (I am currently writing rv64 assembly I run an x86_64).
> […] But we would need explicit atomics, explicit memory barriers, […]
You should read a change summary before complaining about bits missing from C99 that have in fact been added to C11.
> […] no toxic attribute like "packed structure" which makes us lose sight of data alignment […]
And you should also familiarize yourself with what's in actual ISO C vs. compiler extensions before complaining about bits that are in fact compiler extensions.
I TEND to stick to C99 = usually C99 with very few bits of c11+ (usually the missing bits) and even some extensions (often related to ELF/object format). But I really try hard to minimize their usage.
The pb is in ISO c11+ we got some of the missing stuff for modern hardware architecture, but also tons of tantrums (_generic, typeof, restrict....)
> Do they still use 0-terminated strings/char* as the main string type?
Of course, it's still C.
> Is the usage of single linked lists still prevalent as the main container type?
As far as I can remember, the C standard library has never had any functions that used linked lists. Nor are there any container types, linked lists or otherwise, provided by C. So I'd say this is a question about how people teach and use C, not related to the language -- or language spec version -- itself.
> The storage order, the endianness, as given for my machine, is called little-endian. A system that has high-order representation digits first is called big-endian. Both orders are commonly used by modern processor types. Some processors are even able to switch between the two orders on the fly.
Calling big endian "commonly used by modern processor types" when s390x is really the only one left is a bit of a stretch ;D
(Comments about everyone's favorite niche/dead BE architecture in 3… 2… 1…)
Arm is bi-endian and is alive in most phones.
I agree with another GP's comment that modern doesn't mean popular/widely used.
The book does say "Both orders are commonly used by modern processor types". I'd say this sentence is quite misleading, since it would lead you to believe two falsehoods:
1. That both byte orders are equally prevalent in the wild, particularly in systems that are expected to run modern C code.
2. That both byte orders are equally likely to be found in "modern" (new or updated) processor design.
It's not entirely incorrect, but a better phrasing could be used to clarify that little-endian is the more modern and common storage order, but you still cannot ignore big-endian.
Don't a bunch of web protocols use big endian?
You can go lower than that, TCP/IP itself is big-endian (see RFC 1700).
I really doubt any mainstream smartphone runs their Arm chip in big-endian mode ever.
That's besides the point. The book's author has a valid point. Being pedantic should be applied at all levels if you're going that route.
The problem about being pedantic is that you can choose different directions to be pedantic in. My "direction" is that code isn't written in a vacuum, it mixes with code millions of other people wrote and runs on machines millions of other people built. As such:
My concern isn't that the phrasing in the book is wrong, and I have expressly not argued that. It's that it presents the issue as having no further depth, and these two choices as equivalent. They aren't. The "Some processors are even able to switch between the two orders on the fly." that follows makes it even worse, at least to me it really sounds like you needn't give any care.
And the people reading this book are probably the people who should be aware of more real-world background on endianness, for the good of the next million of people dealing with what they produced.
MipsBE is very common in edge devices on many networks. You may have 5 MipsBE devices in your home or office without realizing. It's almost never an issue so nobody cares, but they are common.
> Calling big endian "commonly used by modern processor types" when s390x is really the only one left is a bit of a stretch ;D
POWER is bi-endian. In recent versions, Linux on POWER is little-endian (big-endian Linux on POWER used to be popular, until all the distros switched some years back), while AIX and IBM i are big-endian.
AIX and IBM i are probably not quite as alive as IBM mainframes are, but AIX is still arguably more alive than Solaris or HP/UX are, to say nothing of the dozens of other commercial Unix systems that once existed. Likewise, IBM i is just hanging on, yet still much more alive than most competing legacy midrange platforms (e.g. HP MPE which has been officially desupported by the vendor, although you can still get third party support for it.)
MIPS is still quite alive in consumer networking hardware.
True - but at the same time, about half¹ of it is mipsel, i.e. in little-endian mode :). It's also in decline, AFAICS there is very little new silicon development.
¹ on the OpenWRT table of hardware
Learning MIPS assembly currently using mars and QtSpim.
Any recommended hardware I should use for bare metal development messing around? Hopefully priced like a SBC like the raspberry pi.
Want to move from making basic programs like adding, messing with functions, etc and bring my MIPS assembly up to a real hardware environment.
Many routers use the MIPS ISA and they can be rooted to get shell access. That's what I did with an old Netgear router, which was like a very low spec SBC. If you have a PS2 lying around, you could try that.
well in a way all processors commonly use them... as big-endian is also the network byte order
...x86 CPUs actually have special mov instructions now to load big endian data. Not sure since when though (on godbolt it needs `-march=native`:
https://www.godbolt.org/z/bWfhGx7xh
...without -march=native it's a mov and bswap (so not too bad either).
Looks like it was introduced with Haswell (2013). So it’s safe to use if you’re also compiling with AVX2.
Isn't Sparc big-endian?
Is SPARC still seeing serious hardware development like s390x? I know it's still around but I can't recall the last time I heard of any new hardware.
well, network byte order is a thing. Not a processor though.
"Modern" doesn't mean "currently widespread".
Indeed, if it meant "currently widespread" there'd be a stronger argument for Big Endian with a lot of MIPS and PPC chugging away silently. But interpreting "modern" as recent development, BE is close to gone.
Is there some end to this criticism or do you have some stake in dismissing big endian architectures?
Uh, why so serious? I called it "a bit of a stretch ;D" - there was a reason for that smiley. I'm well aware BE is alive enough to be around.
If you can't live without knowing, sure, my stake in dismissing big endian architectures is that I can't in fact dismiss BE architectures because I have users on it. And it's incredibly painful to test because while my users have such hardware, actually buying a good test platform or CI system is close to impossible. (It ended up being Freescale T4240-QDS devkits off eBay. Not a good sign when the best system you can get is from a company that doesn't exist anymore.)
And at some point it's a question about network protocols/encodings being designed to a "network byte order" determined in the 80s to be big endian. When almost everything is LE, maybe new protocols should just stick with LE as well.
To be fair to IBM, with s390x they do have a "community cloud" programme where open source projects can apply to get a Linux s390x VM to use for things like CI: https://community.ibm.com/zsystems/l1cc/ . But yeah, BE MIPS is super awkward because the target systems are all embedded things that are bad dev/CI machines.
Stupidly enough, "my" software is a routing control plane (FRRouting), so what I need to support are exactly those embedded things. I'm not sure anyone uses FRRouting on a s390x machine. But maybe we should go ask IBM anyway, a BE system is a BE system…
qemu CPU emulation exists too, but that's painfully slow for an actual CI run, and I'm not sure I trust it enough with e.g. AF_NETLINK translation to use the "-user" variant on top of an LE host rather than booting a full Linux (or even BSD).
And in the very best case, proper testing would pit BE and LE systems "against" each other; if I run tests on BE against itself there's a good risk of mis-encodings on send being mis-decoded back on receive and thus not showing as breakage…
… really, it's just a pain to deal with. Even the beauty (in my eyes) of these T4240 ppc64 systems doesn't bridge that :(
> there was a reason for that smiley
Fair—my bad, I can fail at reading tone sometimes.
Would you propose the C abstract machine abstracting away endianness entirely as an alternative? My understanding is that deprecating support for existing architectures is discouraged to every practical extent.
Maybe we failed to communicate because our brains have different endianness? :D
To be honest, I don't think this is a solvable problem. (Changing the C machine concept doesn't do much if you need to process network traffic that uses both (e.g. IP packet [big endian] carrying protobuf [little endian]). It's already mostly a question of data ingress/egress.)
What is solvable though is making sure people are sufficiently aware. And the people who read a book like "Modern C" are probably a very good target audience, building low-level bindings and abstractions. They should know that LE and BE are technically a free-floating design choice, but practically the vast majority of systems is LE now. But at the same time, yes, BE isn't extinct, and won't be any time soon… and it's left to them to make their best possible design given their environments.
Most important aspect of C is its portability. From small microcontrollers to almost any computing platform. I doubt that any new version of C will see that much adoption.
If I want to live on cutting edge I would rather use C++2x or Rust rather than C.
Am I missing something? What benefit this supposedly modern C offers?
One advantage of writing C code is that you don't have annoying discussions about what idiomatic code is supposed to look like, and what language subset is the right one ;)
For the cutting edge I would recommend Zig btw, much less language complexity than both modern C++ and Rust.
One good but less visible side effect of C23 is that it harmonizes more syntax with C++ (like ... = {} vs {0}) which makes it a bit less annoying for us C library maintainers to support the people how want to compile their C code with a C++ compiler.
> C library maintainers to support the people how want to compile their C code with a C++ compiler.
Just tell them to go away.
Trying to write the subset of C and C++ is a fool's errand.
No inline functions in library headers, then.
Inline is mostly pointless in C anyway though.
But it might be a minor problem for STB-style header libraries.
It's not uncommon for C++ projects to include the implementation of an STB-style header into a C++ source file instead of 'isolating' them in a C source file. That's about the only reason why I still support the common C/C++ subset in my C libraries.
There is enough material in C, and related compiler extensions, to have similar discussions, starting from where to place brackets.
Maybe the C24 will define the One Right Way.
Of course they will, just like they did in the past with C11, GNU extensions, or some of the individual features that are now rolled into C23. For example, the 0b notation for binary numbers is widely used in the MCU world.
The microcontroller toolchains are generally built on top of GCC, so they get the features for free. There are some proprietary C compilers that are chronically lagging behind, but they are not nearly as important as they used to be two decades ago.
these features will eventually trickle down into the mainstream, kind of like C11 is doing at the moment
also, unless you're targeting embedded or a very wide set of architectures, there's no reason why you couldn't start using C23 today
Or in other words, for embedded and existing code: most use c99, some use c11 and nobody uses c23 until at least 10 years from now.
This depends on the platform. Many embedded systems are based on arm these days and have modern toolchains available.
I cannot remember the last time I saw C99 used. C codebases generally use C11 or C17, and C++ code bases use C++20
Unless you can vouch for the C++ compiler, the best C++ portable code can offer today is C++17.
Also 8 and 16 bit embedded toolchains are certainly not on C11 / C17, they can hardly afford full C89.
SDCC is a niche C compiler for 8-bit CPUs and is more uptodate than MSVC ;P
https://sdcc.sourceforge.net/
That's the nice thing with C: it's much easier for small teams to fully support than the latest C++ standards.
Now try to use it on the embedded deployments that require certification.
Most devices that are 6+ years old (as far as I can tell) use C99. If not C89. And/or C++17, if that.
That's A LOT of devices out there. A lot of which still get maintenance and even get feature updates (I'm working on one right now, C99).
So the claim that "C codebases generally use C11 or C17, and C++ code bases use C++20" intuitively sounds like totally untrue to someone working in embedded C/C++. I've been doing this for 15+ years and I've never touched anything higher than C99 or C++17.
If you're talking about gaming, sure. But that's not "C code bases generally".
most non-embedded and non-legacy codebases could use c23, that's not an insignificant set
I would argue that is an insignificant set.
Unless you think that code-bases created in the past year are a significant part of code bases that have been created since the inception of humanity.
The `thread_local` specifier is used on a few microcontroller platforms already, but would be absolutely illegal in C11 and before to use. However, it vastly simplifies memory management in a threaded context.
Why would I rather step into the world of C++ just to deal with that?
IIRC, performance and cost of thread local store varies greatly between platforms.
You have to know what you're biting into, before you use that.
Won't any llvm/gcc supported target get the new version of C automatically? You won't get it in the vendor-modified ancient gcc toolchain for some other arch though.
There are many embedded platforms that do not gcc/llvm based compilers.
Also most companies making those platforms are not good at updating their toolchains. Expecting developers to compile their own toolchain, that is unsupported by platform vendor, is too much to ask.
Also GCC dropped support for certain architectures along the way, and even if you are willing to compile your own toolchain, it may not work for you.
I'm with you on this. The feature list reads like a subset of later C++ standards that fit within C's (deliberately) rudimentary feature set.
You could, in theory, just use C++ and be done with it. But like any C++ project you'd need a pretty strict style guide or even a linter, but this time it would have to be extra restrictive lest you slide into full C++ territory. And maybe that's a major stumbling block for some people?
Personally this[1] just makes C much more complicated for me, and I choose C when I want simplicity. If I want complicated, I would just pick C++ which I typically would never want. I would just pick Go (or Elixir if I want a server).
"_BitInt(N)" is also ugly, reminds me of "_Bool" which is thankfully "bool" now.
[1] guard, defer, auto, constexpr, nullptr (what is wrong with NULL?), etc. On top of that "constexpr" and "nullptr" just reeks of C++.
That said, Modern C is an incredible book, I have been using it for C99 (which I intend to continue sticking to).
> what is wrong with NULL?
One of the few advantages of ISO standardization is you can just read the associated papers to answer questions like this: https://wg21.link/p2312
The quick bullet points:
* Surprises when invoking a type-generic macro with a NULL argument.
* Conditional expressions such as (1 ? 0 : NULL) and (1 ? 1 : NULL) have different status depending how NULL is defined
* A NULL argument that is passed to a va_arg function that expects a pointer can have severe consequences. On many architectures nowadays int and void* have different size, and so if NULL is just 0, a wrongly sized argument is passed to the function.
> If I want complicated, I would just pick C++ which I typically would never want
In my opinion, complexity doesn't scale linearly like this. Sometimes, in fact often times, having more complex tools means a simpler process and end result.
It's like building a house. A hammer and screwdriver are very simple. A crane is extremely complex. But which simplifies building a house? A crane. If I wanted to build a house with only a hammer and screwdriver, I would have to devise incredibly complex processes to get it done.
You see the same type of thing in programming languages. Making a generic container in C++ is trivial. It's very, very hard in C. You can make it kind of generic. You can use void * and do a bunch of manual casting. But it's cumbersome, error prone, and the code is more complex. It's counter-intuitive - how can C, a simpler language, produce code that is more complex than C++?
Or look at std::sort vs qsort. The power of templates and functors makes the implementation much simpler - and faster! We don't have to pass around void * and dereference them at runtime, instead we can build in comparison into the definition of the function itself. No redirection, no passing on the stack, and we can even go so far as to inline the comparison function.
There's really lots of examples of this kind of stuff. Point being, language complexity does not imply implementation complexity.
In my experience, complex tools encourage fluffy programming. You mention a generic container; if I were using C, I just wouldn't use a generic container; instead, I'd specify a few container types that handle what needs handled. If there seem to be too many types, then I immediately start thinking that I'm going down a bad architecture path, using too many, or too mixed, abstraction layers, or that I haven't broken down the problem correctly or fully.
The constraints of the tool are inherited in the program; if the constraints encourage better design, then the program will have a better design. You benefit from the language providing a path of least resistance that forces intentionality. That intentionality makes the code easier to reason about, and less likely to contain bugs.
You do pay for this by writing more boilerplate, and by occasionally having to do some dirty things with void pointers; but these will be the exception to the rule, and you'll focus on them more since they are so odd.
Sometimes, but I would argue that C is too simplistic and is missing various common-sense tools. It's definitely improving, but with things like namespaces there's pretty much no risk of "too complex" stuff.
Also, I wouldn't be saying this if people didn't constantly try to recreate C++-isms in C. Which sometimes you need to do. So, then you have this strange amalgamation that kind of works but is super error prone and manual.
I also don't necessarily agree that C's constraints encourage better design. The design pushes far too much to runtime, which is poor design from a reasoning point of view. It's very difficult to reason about code when even simple data models require too much indirection. Also, the severely gimped type system means that you can do things you shouldn't be able to do. You can't properly encode type constraints into your types, so you then have to do more validation at runtime. This is also slightly improving, starting with _Bool years ago.
C++ definitely is a very flawed language with so, so many holes in its design. But the systems it has in place allows the programmer to more focus on the logic and design of their programs, and less on just trying to represent what they want to represent. And templates, as annoying as the errors are, prevent A LOT of runtime errors. Remember, every time you see a template that translates into pointers and runtime checks in C.
I think that is fair. A simple language with a simple memory model is nice to work with.
I also think that it wouldn't be bad for code to be more generic. It is somewhat unnecessary for a procedure to allow an argument of type A but not of type B if the types A and B share all the commonalities necessitated by the procedure. Of course procedures with equivalent source code generate different machine code for different types A or B, but not in a way that matters much.
I believe it is beneficial for the language to see code as the description of a procedure, and to permit this description to be reused as much as possible, for the widest variety of types possible. The lack of this ability I think might be the biggest criticism I have for C from a modern standpoint.
I feel that if C had tagged unions and a little sugar you could write non magical generic functions in C. Non magical meaning unlike C++ etc instead of the compiler selecting the correct function based on the arguments the function itself can tell and handle each case.
Basically you can write a function that takes a tagged union and the compiler will passed the correct union based on named arguments.
auto is mostly useful when tinkering with type-generic macros, but shouldn't be used in regular code (e.g. please no 'almost always auto' madness like it was popular in the C++ world for a little while). Unfortunately there are also slight differences between compilers (IIRC Clang implements a C++ style auto, while GCC implements a C style auto, which has subtle differences for 'auto pointers' - not sure if those differences have been fixed in the meantime).
_BitInt(N) isn't typically used directly but typedef'ed to the width you need, e.g.
The 'ugly' _B syntax is needed because the combination of underscore followed by a capital letter is reserved in the C standard to avoid collisions with existing code for every little thing added to the language (same reason why it was called _Bool).AFAIK defer didn't actually make it into C23?
I'm also more on the conservative side when it comes to adding features to the C standard, but IMHO each of the C23 additions makes sense.
> AFAIK defer didn't actually make it into C23?
Correct, defer didn't make it into C23.
It (in its __attribute__((cleanup())) form) is also one of the most useful extensions in GCC/clang — but, again, for use in macros.
> IIRC Clang implements a C++ style auto, while GCC implements a C style auto, which has subtle differences for 'auto pointers' - not sure if those differences have been fixed in the meantime
Both have compatibly implemented the standard C++ auto. Since 2011 or so.
Well, not in C :)
Here's an example where Clang and GCC don't agree about the behaviour of auto in C23:
https://www.godbolt.org/z/WchMK18vx
IIRC Clang implements 'C++ semantics' for C23 auto, while GCC doesn't.
Last time I brought that up it turned out that both behaviours are 'standard compliant', because the C23 standard explicitly allows such differing behaviour (it basically standardized the status quo even if different compilers disagreed about auto semantics in C).
PS: at least Clang has a warning now in pedantic mode: https://www.godbolt.org/z/ovj5r4axn
> PS: at least Clang has a warning now in pedantic mode: https://www.godbolt.org/z/ovj5r4axn
Did you mean gcc? Your link shows a gcc error:
No, GCC is right to error there, because the code uses a C++-ism (the '*' after 'auto' only makes sense in C++ but not in C).
This difference of implementation in two of the major C compilers leaves a bad taste in my mouth. :/
> (what is wrong with NULL?)
The old definition did not even specify wether it was a pointer or an integer. So for platforms that did not follow the Posix ((void*)0) requirement it was a foot gun that had neither the type nor the size of a pointer.
> On top of that "constexpr" and "nullptr" just reeks of C++.
Probably because they where back ported from C++. You can still use NULL, since that was apparently redefined to be nullptr.
What platforms are those that are in use, and how widespread their use is?
> what is wrong with NULL?
This code has a bug, and may even crash on some architectures:
This code doesn't have the bug: Neither does this:> what is wrong with NULL?
For starters, you have to #include a header to use it.
And it avoids the NULL == 0 ambiguity, allowing for better type checking.
Well, I always include stdio.h which includes stddef.h that defines NULL as (void *)0.
In my experience, hardly any source files require studio.h
stddef.h on the other hand is required by most to get size_t
You are right. Hereby I correct my parent comment: I talked about my own personal experience[1], but yeah, as you said, stddef.h is often required (and yes, often I do not need stdio.h, stddef.h is what I need) which defines NULL, which was my point. If it is often required, then it does not matter whether you have to include a header file or not, IMO.
Just include the stddef.h header if you want to use NULL, similarly to how you include a header file if you want to use anything else, e.g. bool from stdbool.h.
[1] I am not entirely sure in retrospect, actually, as I might be misremembering, but my point stands with or without stdio.h!
NULL is not wrong. The things that I will do with NULL are
I was going to ask if there is a good list of C books and then answered my own question. It categorizes _Modern C_ as Intermediate level.
https://stackoverflow.com/questions/562303/the-definitive-c-...
I like Modern C. I have reviewed it favorably in several places. I agree it is intermediate.
I think 21st Century C by Ben Klemens and C Programming a Modern Approach by King are both more approachable alternatives as a modern C companions to K&R.
Also see Fluent C: Principles, Practices and Patterns by Christopher Preschern.
Note that this is not a complete list, fwiw. For example, I doesn't include "Effective C." [1].
I like "Effective C" over "Modern C" because it's more engaging ... "Modern C" is super rigorous and feels a bit like reading an annotated spec of the language, which is what an expert may need, but makes for a dull read for a casual C user like me.
--
1: https://nostarch.com/effective-c-2nd-edition
I agree, but I think Modern C has good, structured recommendations that make it worth getting through at least once.
I kind of like some of Metaware's high C extensions.
https://news.ycombinator.com/item?id=41647843
https://news.ycombinator.com/item?id=38938402
I've been using modern C++ for a personal project (a language interpreter) for the last year+. I constantly think of switching to C, because of the mental burdens of C++, and because of the problems with tooling (Visual Studio's IntelliSense still barely works, because I use C++20 modules), and compile times get ugly because of the way the language failures force so much into interfaces (even with modules). But on the flip side I've gotten so used to classes, member functions, generic programming (templates), namespaces... I may be hooked.
I've been using C++ for the longest time, and I would never give up destructors to switch to C.
For your particular use case, have you considered C#? VS works much more nicely with it.
Yeah, I did. I want something low level and cross platform, including mobile. I think when I tried the C# for iOS stuff, nothing worked. But it's probably too much VM/runtime for me anyway, for this project.
iOS C# is more or less fine, there is quite a bit of work done in .NET to make this better still. .NET 9 gains native Swift Library Evolution ABI support even - you can literally declare DllImports against public Swift APIs by simply annotating them with [typeof(CallConvSwift)], it's not as convenient as it sounds but it's only a matter of time when the tools like https://github.com/royalapplications/beyondnet adopt this. It's going to get much better once MonoAOT is replaced with NativeAOT for all major publish modes for iOS.
Fair enough. I would've used C++ as well.
Table of contents in the sidebar doesn't work properly for me when I click on an entry (in macOS Preview).
I just test some links in the table of content, works fine for me. Using zathura pdf reader.
Also works in Adobe and Firefox, but doesn't work in Chrome and Edge.
Doesn't work for me either... but I will not dismiss the book because of that.
Table of contents is definitely broken right now.
Same here.
So happy that we still get the dinosaur mascots! This is a good book.
It's only been a few years since I've come to feel I can rely on C compilers all supporting C99, for a library I'm maintaing [1]. And after a couple of years, sure enough - I get an issue opened asking for C89 compatibility because of some arcane embedded toolchain or what-not.
So, C23? ... that's nice and all, but, let's talk about it in 20 years or so T_T
[1]: https://github.com/eyalroz/printf
Can someone link me to an article that explains why C is basically frozen at C99 for all practical purposes? Few projects worth talking about leverage features from C11 and newer
C99 is still new! Microsoft tried to kill C by refusing to implement anything that wasn't also in C++. MSVC was 16 years late implementing C99, and implemented only the bare minimum. Their C11 implementation is only 11 years late.
I suspect that decades of C being effectively frozen have caused the userbase to self-select to people who like C exactly the way it is (was), and don't mind supporting ancient junk compilers.
Everyone who lost patience, or wanted a 21st century language, has left for C++/Rust/Zig or something else.
Most of us liking a good language just did not use MSVC. I do not think many people who appreciate C's simplicity and stability would be happy with C++ / Rust. Zig is beautiful, but still limited in many ways and I would not use it outside of fun projects.
I don't even use Windows, but I need to write portable libraries. Unfortunately, MSVC does strongly influence the baseline, and it's not my decision if I want to be interoperable with other projects.
In my experience, Windows devs don't like being told to use a different toolchain. They may have projects tied to Visual Studio, dependencies that are MSVC-only or code written for quirks of MSVC's libc/CRT, or want unique MSVC build features.
I found it hard to convince people that C isn't just C (probably because C89 has been around forever, and many serious projects still target it). I look like an asshole when I demand them to switch to whole another toolchain, instead of me adding a few #ifdefs and macro hacks for some rare nice thing in C.
Honestly, paradoxically it's been easier to tell people to build Rust code instead (it has MSVC-compatible output with almost zero setup needed).
The good news is that MSVC has C17 support (still missing important optional features, but at least some progress).
Microsoft basically sabotaged C99 by not implementing any of its features until around 2015 in the Visual Studio C compiler, and then still took until 2019 before they acknowledged their failure and started supporting more recent C versions again (MSVC is still reliably behind Clang and GCC when it comes to their C frontend though).
And back around 2010 MSVC still mattered a lot (which sounds weird from today's pov where most developers appear to have moved to Linux).
But OTH, few projects actually need C11 features (and C11 actually took one thing away from C99: VLAs - nothing of value was lost though).
C23 might be the first version since C99 that's actually worth upgrading to for many C code bases.
C11 gave us one very important thing: a standardized memory model! Just like in C++11, you can finally write cross-platform multithreaded code with standardized atomics, synchronization primitives and threads. Unfortunately, compiler/library support is still lacking...
My kingdom for fully specified, well defined portable bitfields.
I wish auto in C was similar to auto in C++.
Wow, the use of attributes like [[__unsequenced__]], [[maybe_unused]] and [[noreturn]] throughout the book is really awful. It seems pretty pedantic of the author to litter all the code examples with something that is mostly optional. For a second I wondered if C23 required them.
Such is the issue with bad defaults. Opting into the sensible thing makes most of your code ugly, instead of just the exceptions.
Really looking forward to #embed, once the compilers catch up. Until then, Golang.
This is not how C standards work. If it appears in the standard, it means that it is already implemented in some compilers (in that case, at least in gcc and clang).
That isn't really how it goes, that is how it used to be up to C99.
Thanks for the correction! Do you know if there is a document from the standards body explaining the change in philosophy?
It's a nuisance to implement the thing you want to add to the standard yourself. It's easier to ship it in the language and then complain at compiler devs that they're running behind the edge of progress.
This interacts in the obvious way with refusing to correct mistakes after the fact for fear of breaking user code.
I don't believe anyone has written a paper along the lines of "let's not bother with the existing practice part anymore", it's more an emergent feature of people following local incentive structures.
I've heard something along the lines of "the standard is to define facilities that will be used in most programs, and to codify widespread existing practice." That was in the context of "I don't like this proposed feature," though. This was for C++, not C.
A lot of stuff in the C++11 standard library was based on widespread use of Boost. Since then, I don't know. Also, were things like templates and lambdas implemented as compiler extensions before standardization? I don't know, but I doubt it. Maybe "we're a committee of people who will decide on a thing and we hope you like it" was always the norm in many ways.
I do that with ld and objcopy:
https://stackoverflow.com/questions/58815959/include-binary-...
Or
:)The anti-Rust approach!
I end up using a .S asm file with .incbin directives to embed files.
#embed would be much nicer
Incbin works just fine from inline asm fwiw
Inline assembly isn't supported for x86-64 and ARM on MSVC which unfortunately also means the incbin trick can't be used there anymore.
Clang 19 has it.
One of my favorite books ever.
How does "Modern" C compare safety-wise to Rust or Zig?
Modern C still promptly decays an array to a pointer, so no array bounds checking is possible.
D does not decay arrays, so D has array bounds checking.
Note that array overflow bugs are consistently the #1 problem with shipped C code, by a wide margin.
> no array bounds checking is possible.
This isn’t strictly true, a C implementation is allowed to associate memory-range (or more generally, pointer provenance) metadata with a pointer.
The DeathStation 9000 features a conforming C implementation which is known to catch all array bounds violations. ;)
> The DeathStation 9000 features a conforming C implementation which is known to catch all array bounds violations. ;)
That actually really does exist already with CHERI CPUs, whose pointers are tagged with "capabilities," which catch buffer overruns at runtime.
https://tratt.net/laurie/blog/2023/two_stories_for_what_is_c...
https://msrc.microsoft.com/blog/2022/01/an_armful_of_cheris/
Right. Also it might it sound like array-to-pointer decay is forced onto the programmer. Instead, you can take the address of an array just fine without letting it decay. The type then preserves the length.
C: int foo(int a[]) { return a[5]; }
Oops.D: int foo(int[] a) { return a[5]; }
Ah, Nirvana!How to fix it for C:
https://www.digitalmars.com/articles/C-biggest-mistake.html
You need to take the address of the array instead of letting it decay and then size is encoded in the type:
Or for run-time length: https://godbolt.org/z/dxx7TsKbK\*Contrast with how simple it is in D:
and the proof is shown by array overflow bugs in the wild are stopped cold. It can be that simple and effective in C.\* what operator is this? I have never seen it. Where can I read about it?
My guess is that it was intended to escape the * since unescaped * in regular text on HN results in italics. Since the text in question is in a code block, though, that escaping is not needed.
This should be caught by CHERI.
Nice, when you know the length at compile time, which is rarely from my experience.
The holy grail is runtime access to the length, which means an array would have to be backed by something more elaborate.
Oh, it also work for runtime length:
https://godbolt.org/z/PnaWWcK9o
Now try that on a compiler without -fsanitize=bounds, yet full ISO C compliant.
You can still access the size which is what the parent was asking for. And please tell me how you would try this on an ISO compliant compiler for D.
D has bounds checking, and isn't a ISO language.
A worked example: https://github.com/pizlonator/llvm-project-deluge/blob/delug...
"The DeathStation 9000"
The what now?
Nasal daemons for those of us of a slightly older vintage ...
Google it.
Yeah, why have any type of human interaction in a forum when you can just refer your fellow brethren to the automaton.
I’m saying this because any explanation I could offer would provide less insight than the Google results.
Less insight, perhaps, but of higher quality, which is subjective.
I personally find that googling stuff provides not much connection to the subject of study, very impersonal and try to avoid it.
For example I did google the concept, and found this https://github.com/cousteaulecommandant/ds9k.
Which is not trivial to parse, bing posited the answer as authoritative, and if you look at the code it is really nothing, it seems to be a folklore concept, and as such, it is much more aptly transmitted by speaking to a human and getting a live version than by googling an authoratitative static answer.
The thing is though that even with array bounds checking built into the language, out of bounds access due to programming error can still be attempted. Only this time it's safer because an attacker can't use the bug (which still exists) to access memory outside of bounds. In any case, the program still doesn't work as intended (has bugs) because the programmer has attempted, or allowed the attempt, to access out of bounds memory.
Writing safe code is better than depending on safety features. Writing safe code is possible in any programming language, the only things required are good design principles and discipline (i.e. solid engineering).
In practice in C, that does not work because array overflow bugs are still the #1 bug in shipped C code, by a wide margin.
You'd be surprised: Zig has one UB (Undefined Behaviour) that C doesn't have!
In release fast mode, unsigned overflow/underflow is undefined in Zig whereas in C it wraps.
:-)
Of course C has many UBs that Zig doesn't have, so C is far less safe than Zig, especially since you can use ReleaseSafe in Zig..
UB is does not automatically make things unsafe. You can have a compiler that implements safe defaults for most UB, and then it is not unsafe.
That's implementation defined behavior, not undefined behavior. Undefined behavior explicitly refers to something the compiler does not provide a definition for, including "safe defaults."
The C standard says, and I quote:
>Possible undefined behavior ranges from ignoring the situation completely with unpredictable results ... or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message)
So a compiler is absolutely welcome to make undefined behavior safe. In fact every compiler I know of, such as GCC, clang, MSVC has flags to make various undefined behavior safe, such as signed integer overflow, type punning, casting function pointers to void pointers.
The Linux kernel is notorious for leveraging undefined behavior in C for which GCC guarantees specific and well defined behavior.
It looks like there is also the notion of unspecified behavior, which gives compilers a choice about the behavior and does not require compilers to document that choice or even choose consistently.
And finally there is what you bring up, which is implementation defined behavior which is defined as a subset of unspecified behavior in which compilers must document the choice.
Compilers are not prohibited to provide their own definition for UB, that's how UBsan exists.
Well Zig has ReleaseSafe for this.. ReleaseFast is for using these UBs to generate the fastest code.
By definition UB cannot be safe.
Something can be UB according to the standard, but defined (and safe) according to a particular implementation. Lots of stuff is UB according to the C or C++ standard but does something sensible in gcc and/or clang.
The definition given by the C standard allows for safe undefined behavior.
this depends on your choice of definition for "safe"
Does C automatically wrap? I thought you need to pass `-fwrapv` to the compiler to ensure that.
Unsigned overflow wraps. Signed overflow is undefined behavior.
This distinction does not exist in K&R 2/e which documents ANSI C aka C89, but maybe it was added in a later version of the language (or didn't make it into the book)? According to K&R, all overflow is undefined.
I don't have my copy of K&R handy, but this distinction has existed since the initial codification. From C89:
Source: C89 (draft) at https://port70.net/~nsz/c/c89/c89-draft.txt-fwrapv is for signed integer overflow not unsigned.
Yes, as unsigned overflow is fine by default. AFAIK the issue was originally that there were still machines that used ones complement for describing negative integers instead of the now customary twos complement.
Modern C is barely any different than older C. The language committee for C is extremely conservative, changes tend to happen only around the edges.
Except for C99 which added designated init and compound literals. With those it almost feels like a new language compared to C89 (and the C99 designated init feature is so well thought out that it still beats most similar initialization patterns in more recent languages, including C++, Rust and Zig - only Odin seems to "get it").
There's finally a way to safely add two signed numbers, without tricky overflow checks that may trigger UB themselves!
Wait, C programmers now put the star on the left hand side?
char* thing; // good
char *thing; // bad
This ... is awesome. As a C++ "native" I've always found the "star on the right" thing to be really horribly confusing.
Ofc this has always been an option. In my C heyday I used to put a space on both sides of the star. It makes for a more consistent syntax when you have multi layer pointers with const at various layers. For example:
// mutable pointer to mutable data:
char * str;
// Immutable pointer to immutable data:
char const*const str;
// Mutable pointer to an immutable pointer to a mutable pointer to immutable data:
char const**const* strs;
Given that putting it on the right reflects the actual syntax tree of the code, why do you find it "horribly confusing"?
I mean, one can reasonably argue that C & C++ declarator syntax is itself horribly confusing because it doesn't read left-to-right. But it is what it is, so why pretend that it's something else?
any chance of getting a responsive TOC in any pdf reader whatsoever?
https://www.manning.com/books/modern-c-third-edition
in example 1.1 i read that as 'tits_square' until i saw the output
that's by design
It's a booby trap.
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.
{fmt} used to support pre-C++11 compilers that didn't have variadic templates. It was a pain to emulate variadics but it wasn't impossible.
Wow, I don't know that! I would be curious to know how they did it.
It was emulated using horrible macros: https://github.com/fmtlib/fmt/blob/8f5e07656e25ec64ec7c843f0.... IIRC some early versions of `std::tuple` did something similar.
Oh, I didn't realize you're the author! Thank you so much for fmt/std::format - it's truely a godsend!
Glad you like it =)
> 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.
>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:
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.I know what reflection is of course. C++ makes it easy to implement IO. If you're asking for a reflection-based solution with less effort, you are practically asking for zero extra code. Anyway, C++ does not yet have reflection but who's to say how anyone wants any particular data to be dumped? A default implementation is nice but less useful than you make it sound. In any case, there are libraries approximating what you described (usually with macros and stuff) and reflection is totally coming at some point.
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.
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.
No C++ programmer who has been around the block more than halfway should do
and assume that it works.But also ...
doesn't help the problem in any substantive way.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.
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.
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.
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.
Considering that they are evolving, I think they are more likely than not to stay standing. There might be other similar languages developed in parallel, but after over 30 years of whining C and C++ are still popular. I don't expect that to change.
_("some text") ... aka gettext and friends.
QT, unfortunately.
Why do you "unfortunately"?
It's like using a sledgehammer for a picture hook. I think QT is a great tool, and it solves this problem nicely, but it's a full blown framework, and if you're already using something else - particularly if it's something "light" like SDL, or just a platform specific library like Win32, it's an awful lot to pull in (plus compile times, licensing, etc).
If you are using SDL, what is wrong with GNU gettext()?
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.
Yep, it’s very clean once you get the hang of it.
This was a tip my hatn excellent to you
Why?
> 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 |.
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?
https://en.cppreference.com/w/cpp/utility/format for C++20 and https://en.cppreference.com/w/cpp/io/print for C++23
https://github.com/fmtlib/fmt is what it's based on, for C++11 and up support.
Sather (1991) used operator overloading for output. And, even more hilariously, they overloaded + in the same way as C++ overloaded <<, giving you:
I did not know this nor indeed that Sather existed. Thanks. I don't feel as though "But Sather did it too" counts as a good reason for I/O streams, but thanks for telling me.
I don't think it's a good reason, and FWIW I'm pretty sure they got the idea from C++ - iostream design predates ISO C++ by quite a bit. I remember seeing << for that with stuff like Borland C++ 3.1 that also originates in 90s, back when you had to write #include <iostream.h> etc. Just noting that this is not such an obviously bad idea that nobody else hasn't fallen into the same trap.
> 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.
I think this is a very lazy and somewhat conspiratorial take.
C++'s IO stream library, along with C++'s adoption of std::string, is a response to and improvement over C's standard library support for IO. That alone makes it an invaluable improvement. It's easy and very lazy to look back 30 years ago and badmouth things done back then.
It's also easy to complain about no one proposing changes when literally anyone, including you, can propose changes. The only need to do the legwork and put their money where their mouth is. The funny part is that we see frameworks putting together their own IO infrastructure and it ends up being not good, such as Qt's take on IO.
But talk is cheap and badmouthing doesn't require a pull request.
The problem is precisely that C++ iostream library was, in practice, not an improvement on C stdio in many ways. Some of us were actually there 30 years ago, and even right after C++98 was standardized, it was pretty common for (then-)modern C++ projects to adopt all of stdlib except for iostreams (and locales/facets, another horrible wart).
What’s wrong with it?
Thank you.
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.
Not only prototypes but typedefs with function pointers as well.
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:
(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
(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.
kinda like python and ruby
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.
> installing Windows will still make your Linux system unbootable until you fix it with a rescue disk
This is no longer true. On UEFI systems the only thing you have to do normally is fix the boot order. In fact installing Linux first and Windows second tends to be the better dual-boot strategy nowadays.
Fixing the boot order can be done from UEFI setup, and even from Windows command line
(Put single quotes around {} if you use PowerShell instead of CMD.exe)Thank you for the correction and the howto! How long have I been wrong about this?
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.
That's an improvement! When did they fix that?
Microsoft doesn't take part on WG14, and MSVC only does up to C17 nowadays.
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.)
You can obviously build any runtime system you desire in C, including one that parses and executes C code with additional features added in. The wisdom of doing this is questionable.
Though I've actually seen macro systems that do things akin to destructors, although less automatically.
Surely you are aware that C++ destructors are essentially made in C.
Namespaces, methods.
Namespaces is not object orientation, is it? Am I missing something? You can place functions (methods) inside of structs in C23, can't you?
You can handcode vtables in C, just as you can handcode loops in assembly (i.e. it works but it's verbose, not particularly readable, and brings more footguns).
But why would you do that if you have an instrument that lets you work at the same level as C, but with methods provided as a proper abstraction that maps exactly to what you'd have written yourself anyway?
I don't know, I never found the "proper abstraction" be more than irrelevant syntactic sugar. And the cost of C++ is that you end up putting everything in the header (IMHO the biggest design flaw of the language) and then compile time start to get long....
At the very least, the proper abstraction is the one that guarantees that you pass the same pointer as the first argument of the method call as the one you used to read the vtable from.
And no, you don't have to put everything in the header with C++, and especially not if you're using it in "C with classes" mode. C++ only needs it for templates - and even then only if you want to use implicit instantiation, with `extern template` available for fine-grained control allowing you to have those specializations separately compiled and implementations kept out of the header file.
On a high level, "object orientation" means you think of your code as representing the state and interactions of objects. You can equally well do this in assembly. If you think of some namespace as a "singleton object" then that's what it is.
I guess what you're really asking is what are the best or most common ways to do OO in C?
Oh. I learned that object orientation is primarily a way to structure data and code, such that the data is encapsulated with the code that works on it, in so called objects. So an Object is the Data, plus the functions that work on the data, an ensure that some invariants are kept. In OO parlance, that code gets executed by sending messages (calling methods).
Where can I find something about objects being "think of your code as representing the state and interactions of objects" honesty totally new to me.
So no, certainly I'm not asking ways to do OO in C. But it seems to be more definitions of object orientation as I thought...
There's no clear definition of what OO is, so the best you can do pragmatically is look at mainstream languages that are broadly recognized as OO and try to deduce the commonalities.
If you do that, you'll notice that, for example, encapsulation is not a part of that de facto definition, because languages like Python and (until recently) JavaScript lack it, despite being considered OO.
Indeed, the only two things that appear to be consistently present in all OO languages are: 1) some notion of object identity as distinct from object state, and 2) runtime polymorphic dispatch.
> Where can I find something about objects being "think of your code as representing the state and interactions of objects" honesty totally new to me.
I’m scratching my head how you think this is materially different than what you described in your first para. s/state/data and s/interactions/methods.
If anything though I would say the GP is more aligned with the classic definition as it highlights the focus is more on the messages (interactions) themselves rather than the implementation.
Correct, and you did ask specifically for OO things, but I thought I'd list namespaces too as far as “C++ things you might use when writing C-like C++ code”.
Another big one that I always forget C still doesn't support is function overloading.
Function overloading is a feature that makes code less self-documenting without providing any meaningful value. Operator overloading is more interesting, because you can build you domain language with nice syntax. But I also tend to think that this is not really worth it.
Function overloading enables the building of powerful idioms like swap, operator== and this [1] proposal for composable hashing to name a few. And when combined with templates (and ADL to provide some encapsulation with namespaces) you can build some interesting abstractions akin to the io module in go, like this implementation of io.ReadFull():
---1: https://open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.h...
In C++ where you have methods? Sure. It would be nice to have in C, though. But, alas, ABI compatibility.
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.
It's very useful syntactic sugar, though.
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.
If you argue for Rust I'm all for it, arguably much less of a learning curve than C++ too.
Strong disagree on that one. Even though C++ has a lot of features that take a while to learn, getting started with C++ is simpler by miles than getting started with Rust.
One exception to that in my experience: dependencies, although I think that's a bit deceiving as yes, it's easier to get dependencies in Rust but in some areas they're way less mature and can sometimes be a pain to work with (usually when dealing with big C or C++ libraries that have been quickly glued to a Rust interface to be available in Rust).
Agree, writing bad C++ is easy. But being competent in C++ requires much more than being competent in rust.
And not being competent in C++ is not great. You are going to be much more productive in C. The feedback loop is much faster, that is, feedback from your designs.
Contrast with Rust which is harder to get going but doesn't require nearly as much to be decent.
CRTP?
Yeah, but one should provide C++ type safe abstractions on top.
Just like one doesn't use Typescript to keep writing plain old JavaScript, then why bother.
I mean as long as your goal is specifically to do that I think it's fine. Using a C++ compiler to compile a C program isn't that rare.
A couple of months ago, in the company I work, there was a talk from HR, where they explained how to make a good CV (the company is firing lots of people). She say: "if you have experience in programming C, you can writing just that, or, if you have lots of experience in C, is customary to write ``C++ Experience'' "
Sooo... yeah... I should definitely change company!
That literally made me do a spit take, and it was fizzy water and it burned.
My god. That's amazing.
How many pluses until you should just say you have D experience?
Possibly three. Four pluses is naturally C#.
can't believe so many people are arguing against this honestly. you don't mix them in the sense the author means. I take it these people didn't read the paragraphs this was the 'takeaway' from.
For example, the primary reason for the sentence seems to be from the text: "Many code examples in this book won't even compile on a c++ compiler, So we should not mix sources of both languages".
It's not at all about the ability to use c libraries in c++ projects or vice versa :S.... c'mon guys!
If you want a language with a great C FFI, C++ is hard to beat!
Bjarne should have called it ++C.
Nah. It's just the natural semantics -- he added stuff to C, but returned something that wasn't actually more advanced...
Because people choose to use pre-increment by default instead of post-increment?
Why is that?
It should be ++C because with C++ the value you get from the expression is the old one.
If you're asking why people use pre-increment by default instead of post-increment, it's mostly historical. The early C compilers on resource-constrained platforms such as early DOS were not good at optimization; on those, pre-increment would be reliably translated to a simple ADD or INC, whereas code for post-increment might generate an extra copy even if it wasn't actually used.
For C++ this was even worse with iterators, because now it depended on the compiler's ability to inline its implementation of postfix ++, and then prove that all the copies produced by that implementation have no side effects to optimize it to the same degree as prefix ++ could. Depending on the type of the underlying value, this may not even be possible in general.
The other reason is that all other unary operators in C are prefix rather than postfix, and mixing unary prefix with unary postfix in a single expression produces code that is easy to misunderstand. E.g. *p++ is *(p++), not (*p)++, even though the latter feels more natural, reading it left-to-right as usual. OTOH *++p vs ++*p is unambiguous.
K&R seems to use pre-increment early on, then post-increment consistently (or a lot, anyway, I haven't done a thorough check) after chapter 3, in situations where either would do. In fact, after introducing post-increment at 2.8.
> It should be ++C because with C++ the value you get from the expression is the old one.
You get it!
The PDP-11 that C originally targeted had address modes to support the stack. Pre-increment and post-decrement therefore did not require a separate instruction; they were free. After the PDP-11 went the way of the dodo, both forms took a machine cycle so it (mostly) became a stylistic issue. (The two operators have different semantics, but the trend to avoid side-effects in expressions means that both are most often used in a single expression statement like "++x;" or "x++;", so it comes down to your preferred style.)
Please explain what you mean by "a separate instruction".
Some idiomatic C code to copy a string (I'm not saying this is good C code, but it's just an example):
On the Motorola 68000 (based somewhat on the PDP-11) the code would look like: while on the x86 line, it would be: Yes, there are better ways to write that code for both the 68K and x86, but I hope this gets the point across.> loop: move.b (a0)+,d0 move.b d0,(a1)+
...
> loop: mov al,[rsi] mov [rdi],al
This hurts my brain. When we invent time machines I'm going to use it to go back and slap whoever at intel came up with that operand order.
memcpy etc also take the destination as their first argument, and it mirrors the usual way we write assignments. Personally I always found Intel syntax to be more straightforward, but I think it's ultimately down to whatever one was exposed to first.
I wonder sometimes why we keep insisting on the "OP ARG, ARG" format in general for assembly. Why not something like `MOV X -> Y` that would make it absolutely clear and unambiguous? For that matter, why not `COPY X -> Y`, since that's what it actually does?
It hurts less if you think of it like assignment:
A MOV and an INC, as opposed to just the MOV.
Why would you use post increment by default? The semantics are very particular.
Only on very rare occasions I need post increment semantics.
And in those cases I prefer to use a temporary to make the intent more clear
I rarely use pre-increment tbh, but post-increment all the time for array indices (since typically the array should be indexed with the value before the increment happens).
If the pre- or post-increment behaviour isn't actually needed, I prefer `x += 1` though.
People seem to mostly write a typical for loop ending with ; ++i){
But I write ; i++){ and seeing it the other way round throws me off for a minute, because I think, as you put it, why would you use those very particular semantics?
But I guess this is only a semantic argument.
In C++ the semantics can differ, in that copying an object for post-increment might require a memory allocation internally (for example in the case of a BigInt class), which may fail and throw an exception. For consistency, using pre-increment by default and unless you really need post-increment, is a good habit.
> why would you use those very particular semantics?
The difference is that i++ has to keep a copy to the original around as the return value is the pre-increment value, while with ++i that isn't needed as the resulting value is being returned.
In the for loop that shouldn't matter as a) for an integer it is essentially for free (it is just reordering when the relevant register is set) and b) that value is hopefully optimized out anyways by the compiler, however as there are cases where it matters some people prefer the ++i style, some just think it looks better.
I've seen either style, but it the argument about which is proper is pointless. Any modern compiler will optimize either equally well, unless you're doing something that actually depends on the order of the increment.
No, for fundamental datatypes pre/post-increment doesn't matter, but for classes that overload those operators, the postfix form creates a temporary object hence people write
for(auto it = begin(v); it != end(v); ++it)
It makes no difference if the increment is done on an int, but it can make a different if your `i` is some object with its own ++ operator.
If you're used to the idiom, the intent couldn't be clearer.
I miss it when switching between C/++ and other languages.
Why use this operator? Like most C and C++ features the main reason tends to be showing off, you learned a thing (in this case that there are four extra operators here) and so you show off by using it even if it doesn't make the software easier to understand.
This is not one of those beginner -> journeyman -> expert cycles where coincidentally the way you wrote it as a beginner is identical to how an expert writes it but for a very different reason. I'd expect experts are very comfortable writing either { x = k; k += 1; } or { k += 1; x = k; } depending on which they meant and don't feel an itch to re-write these as { x = k++; } and { x = ++k; } respectively.
I'm slightly surprised none of the joke languages add equally frivolous operators. a%% to set a to the remainder after dividing a by 10, or b** to set b as two to the power b or some other silliness.
They can be useful when adding things to an array in a loop. A trivial example which removes a character from a null terminated string:
This might be better expressed with a higher order filter function, but C is too low level for things like that.There are also idioms for stack manipulation using them: "stack[sp++] = pushed" and "popped = stack[--sp]".
C code does a lot of incrementing and decrementing by one, and so having dedicated syntax for it is convenient.
Note that in your example there appear to be three distinct meanings:
1. prefix incr/decr precedence: "stack[--sp]"
2. postfix incr/decr precedence: "s[j++]"
3. i have no particular preference for the precedence and am just using a shorthand I inherited from my ancestors whose use cases are no longer relevant to me: "i++" in your for loop
My rank speculation is that C programmers get in a habit of #3 and then forget to consider precedence in an expression where it matters.
In any case, it would be interesting to do a scan of github to see how often prefix and suffix incr/decr had to get switched up in a bugfix patch.
Silly mistake: that should read "s[j++] = s[i]".
The idiomatic
(straight from K&R) wouldn’t work without it.Which many people find unreadable compared to other versions.
And for several reasons.
And I have seen in some places!It is concise syntax but very confusing.
K&R actually teaches this as a desirable idiom? People should not be recommending K&R to beginners today!
Kernighan spends a page whittling strcpy down to just that, with various intermediate versions. After showing you that version, he describes it like this:
Although this may seem cryptic at first sight, the notational convenience is considerable, and the idiom should be mastered, because you will see it frequently in C programs.
It's more useful for pointers than for values, IMO
For iterators, += may not even be available.
I payed for this on manning and they didn’t even release the final version yet. I guess I didn’t understand what I was buying, but I can’t help feel a bit cheated.
Continuing to use a memory-unsafe language that has no recourse for safety and is full of footguns and is frankly irresponsible for the software profession. God help us all.
By the way, the US government did the profession no favors by including C++ as a memory-unsafe language. It is possible to write memory-safe C++, safe array dereferencing C++. But it’s not obvious how to do it. Herb Sutter is working on it with CppFront. The point stands that C++ can be memory-safe code. If you make a mistake, you might write some unsafe code in C++. But you can fix that mistake and learn to avoid it.
When you write C, you are in the bad luck shitter. You have no choice. You will write memory—unsafe code and hope you don’t fuck it up. You will hope that a refactor of your code doesn’t fuck it up.
Ah, C, so simple! You, only you, are responsible for handling memory safely. Don’t fuck it up, cadet. (Don’t leave it all to computers like a C++ developer would.)
Put C in the bin, where it belongs.
You can't just put a language in the bin that has been used for 50 years and that a huge percentage the present day software infrastructure is built on.
I see comments like yours everywhere all the time and I seriously think you have a very unhealthy emotional relationship with this topic. You should not have that much hate in your heart for a programming language that has served us very well for many decades and still continues to do so. Even if C was literally all bad (which imho isn't even possible), you shouldn't be that angry at it.
When you write C++, you can allocate memory all day long and write ZERO delete statements. That is possible, I’ve been writing C++ like that since 1998 (Visual C++ 5.0 and lcc). Can you imagine allocating memory and never risk a premature or a forgotten delete? It is not possible in C. You can call it opinion, but I see fact. That makes C all that bad.
When I say put it in the bin, I don’t mean that good software hasn’t been written already with it, or can’t be written with it. But you should stop using it given the earliest opportunity. When given the ability to write object-oriented software, clever engineers with too much time add insane complexity justified by unproven hypotheticals. Believe me, I know very well why people shy away from C++ like a trauma response. Overly-engineered/overly-abstracted complexity, incomprehensible template syntax, inadequate standard library, indecipherable error messages, C++ has its warts. But it is possible to write memory-safe software in C++, and it is not in C (unless we are talking about little code toys!). My answer is that you don’t have to write complicated garbage in C++. Keep it simple like you are writing C. Add C++ features only to get safety. Add polymorphism only when it solves a problem. Never write an abstract class ahead of time. Never write a class ahead of time.
Downvote me all day long. Call me angry. When billions of dollars are lost because someone, in our modern age, decided to write new software in C, or continue to develop software in C instead of switching to a mixed C++/C codebase with an intent to phase out new development in C.
It’s hard not to get angry when modern software is written with avoidable CVEs in 2020’s. Use after free, buffer overflows, are you kidding me? These problems should have been relics in 2010+, but here we are.
There are still applications (especially with embedded devices) where you do not dynamically allocate memory or might not even use pointers at all.
There good tools that help improving memory safety in C and I do not think Rust is a good language. Of course, the worst about Rust are its fans.
Skill issue
It's been a skill issue for 40 years. How long are we going to continue searching for those programmers who don't make mistakes?
Programmers make stupid mistakes in the safest languages too, even more so today when software is a career and not a hobby. What does it matter if the memory allocation is safe when the programmer exposes all user sessions to the internet because reading Dockers' documentation is too much work? Even Github did a variant of this with all their resources.
Because memory vulnerabilities don't make programs immune to other dumb mistakes. You get these vulnerabilities on top of everything else that can go wrong in a program.
Manual checking of memory management correctness takes extra time and effort to review, debug, instrument, fuzz, etc. things that the compiler could be checking automatically and reliably. This misplaced effort wastes resources and takes focus away from dealing with all the other problems.
There's also a common line of thinking that that because working in C is hard, C programmers must be smarter and more diligent, so they wouldn't make dumb mistakes like the easy-language programmers do. I don't like such elitist view, but even if true, the better programmers can allocate their smarts to something more productive than expertise in programs corrupting themselves.
> programmers can allocate their smarts to something more productive than expertise in programs corrupting themselves
Amen. This is called progress.
Because memory vulnerabilities don't make programs immune to other dumb mistakes. You get these vulnerabilities on top of everything else that can go wrong in a program.
The issue is that these great new tools don't just fix the old vulnerabilities, they also provide a lot of new, powerful footguns for people to play with. They're shipping 2000 feet of rope with every language when all we need is 6 feet to hang ourselves.
There has been a bunch of failed C killers, and C++ has massively shat the bed, so I understand that people are jaded.
However, this pessimistic tradeoff is just not true in case of Rust — it has been focused from the start on preventing footguns, and actually does a great job of it. You don't trade one kind of failure for another, you replace them with compilation errors, and they've even invested a lot of effort into making these errors clear and useful.
GCC support has been around since gcc 11 apparently. See table at (1). This is available in ubuntu 22.04. The page below also shows support for C26!
1) https://gcc.gnu.org/projects/cxx-status.html#:~:text=C%2B%2B...
That's for C++ not C
Whoops, thanks for the correction!
I am worried where "official" C is going. Its syntax which is already too complex and already does too much, but that would require to "break" backward compatibility namely it would require "porting". But since it would be still "C" that amount of work should be close to "a bit" of "step by step" refactoring
For instance, only sized types:u8...s64, f32, f64... no implicit casts except for void* and literals, no integer promotion, no switch, no enum, only one loop keyword (loop{}!), no anonymous code block, and no toxic attribute like "packed structure" which makes us lose sight of data alignment... no _generic, typeof, restrict, syntax based tls, etc...
But we would need explicit atomics, explicit memory barriers, explicit unaligned memory access.
Instead of adding and complexifying C to make writing a naive compiler more and more complex, long and a mouse and cat catchup "to the standard" tedious task, what should be done is exactly the other way around.
In end, I don't trust C officials anymore, I tend to stick to C99, or even assembly (I am currently writing rv64 assembly I run an x86_64).
> I tend to stick to C99,
> […] But we would need explicit atomics, explicit memory barriers, […]
You should read a change summary before complaining about bits missing from C99 that have in fact been added to C11.
> […] no toxic attribute like "packed structure" which makes us lose sight of data alignment […]
And you should also familiarize yourself with what's in actual ISO C vs. compiler extensions before complaining about bits that are in fact compiler extensions.
I TEND to stick to C99 = usually C99 with very few bits of c11+ (usually the missing bits) and even some extensions (often related to ELF/object format). But I really try hard to minimize their usage.
The pb is in ISO c11+ we got some of the missing stuff for modern hardware architecture, but also tons of tantrums (_generic, typeof, restrict....)
Do they still use 0-terminated strings/char* as the main string type?
Is the usage of single linked lists still prevalent as the main container type?
> Do they still use 0-terminated strings/char* as the main string type?
Of course, it's still C.
> Is the usage of single linked lists still prevalent as the main container type?
As far as I can remember, the C standard library has never had any functions that used linked lists. Nor are there any container types, linked lists or otherwise, provided by C. So I'd say this is a question about how people teach and use C, not related to the language -- or language spec version -- itself.
Don't feed the trolls.
I don't mean the language spec but what is commonly used in the wild.
The C standard library provides no recognizable container types, so there's no "main" anything.
I don’t think that will ever change. Will the possibly introduce a more modern string2 type? Maybe but it will probably be unlikely before 2050
Embedded linked lists are pretty cool though.
aka intrusive linked lists