Given how simple examples in this blog post are, I ask myself, why don't we already have something like that as a part of the standard instead of a bunch of one-off personal, bug-ridden implementations?
Given how simple examples in this blog post are, I ask myself, why don't we already have something like that as a part of the standard instead of a bunch of one-off personal, bug-ridden implementations?
If you need to cleanup stuff on early return paths, use goto.. Its nothing wrong with it, jump to end when you do all the cleanup and return. Temporary buffers? if they arent big, dont be afraid to use static char buf[64]; No need to waste time for malloc() and free. They are big? preallocate early and reallocate or work on chunk sizes. Simple and effective.
There are a million internal C apps that have to be tended and maintained, and I’m glad to see people giving those devs options. Yeah, I wish we (collectively) could just switch to something else. Until then, yay for easier upgrade alternatives!
I wholeheartedly agree that a future of not-C is a much better long term goal than one of improved-C.
That's a very interesting belief. Do you see a way to achieve temporal memory safety without a GC, and I assume also without lifetimes?
Would be awesome if someone did a study to see if it's actually achievable... Cyclone's approach was certainly not enough, and I think some sort of generics or a Hindley-Milner type system might be required to get it to work, otherwise lifetimes would become completely unusable.
In a function? That makes the function not-threadsafe and the function itself stateful. There are places, where you want this, but I would refrain from doing that in the general case.
But in program, its not bad. If I ever need multiple calls to it in same thread:
static char buf[8][32];
static int z;
char *p=buf[z];
z=(z+1)&7;
Works pretty well ;)Static foremost means that the value is preserved from the last function invocation. This is very different behaviour, than an automatically allocated variable. So calling a function with a static variable isn't idempotent, even when all global variables are the same.
> If I ever need multiple calls to it in same thread:
What is this code supposed to do???? It hands out a different pointer, the first 8 times, than starts from the beginning again? I don't see what this is useful for!
What application is that for? Embedded, GUI program, server, ...?
My thoughts as well. The only thing I would be willing to use is the macro definition for __attribute__, but that is trivial. I use C, because I want manual memory handling, if I wouldn't want that I would use another language. And now I don't make copies when I want to have read access to some things, that is simply not at a problem. You simply pass non-owning pointers around.
It's just, I'd rather play with my own toys instead of using someone else's toy. Especially since I don't think it would ever grow up to be something more than a toy.
For serious work, I'd use some widely used, well-maintained, and battle-tested library instead of my or someone else's toy.
* `fopen_s`, `freopen_s` deviate in the API: restrict is missing.
* `strtok_s`, `wcstok_s`,`vsnprintf_s` miss the dmax argument.
* `vsnprintf_s` adds a maxarg argument.
* `vswprintf` adds a maxarg argument on w32. (with `__STRICT_ANSI__` undefined)
* no `strnlen` on mingw32.
* no `errno_t` return type for `qsort_s`, only `void`.
* reversed argument order for `localtime_s` and `gmtime_s`.
* older mingw versions have `wchar.h` with only 2 functions: `wcscmp`, `wcslen`
* no `RSIZE_MAX`
* `memmove_s` does not clear dest with ERANGE when `count > dmax` and EINVAL when src is a NULL pointer.
* `vsprintf_s`, `sprintf_s` return `-1` on all errors, not just encoding errors. (Wrong standard)
* With `wcsrtombs` (used by `wcsrtomb_s`) the `retval` result includes the terminating zero, i.e. the result is `+1` from the spec.
`getenv_s` returns in len the size of the env buffer, not the len, as described in the standard (https://en.cppreference.com/w/c/program/getenv). The Microsoft size is len + 1. Their usage example is also wrong: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getenv-s-wgetenv-s?view=msvc-170
A global constraint handler is still by far better than dynamic env handlers, and most of the existing libc/POSIX design failures.
You can disable this global constraint handler btw.
No it is because you still need to get the size calculation correct, so it doesn't actually have any benefit over the strn... family other than being different.
Also a memcpy that can fail at runtime, seems to be only complicating things. If anything it should fail at compile time.
Since the user is mostly wrong with memory bounds, the compiler checks it also. And with clang even allows user-defined warnings.
We all known that C programmers know it better, and hate bounds-checks, that's why there are so many out-of-bounds errors still.
Just don't use C for sending astronauts in space. Simple.
C wasn't designed to be safe, it was designed so you don't have to write in assembly.
Just a quick look through this and it just shows one thing: someone else's walled garden of hell.
But do use C to control nuclear reactors https://list.cea.fr/en/page/frama-c/
It's a lot easier to catch errors of omission in C than it is to catch unintended implicit behavior in C++.
While these are a huge improvement over no extra tooling, they don't compare to analyzers like Frama-C plugins, which demand further annotations/proofs if necessary to show code is free of UB, and you can provide further to show your code is not just safe, but correct. Assuming one doesn't ship rejected code, the latter is pretty much its own language with much stronger guarantees, much like SPARK is to Ada.
I like sanitizers and other compiler-specific guarantees, they at least try to fill the gaps by giving proper semantics to UB. But the ones available for C are still insufficient, and some are very resource-heavy compared to just running safe code. I'm excited about Fil-C showing a path forward here.
Widget w(); // I made a widget, right? RIGHT?
Wrong. You just declared a function that takes no parameters and returns a Widget. The compiler looks at this line and thinks "Ah yes, clearly this person wants to forward-declare a function in the middle of their function body because that's a completely reasonable thing to do."Let's say you wise up and try this:
Widget w(Widget()); // Surely THIS creates a widget from a temporary?
Nope! That's ALSO a function declaration. You just declared a function called w that takes a function pointer (which returns a Widget) as a parameter.The "fix"? Widget w{}; (if you're in C++11 or later, and you like your initializers curly). Widget w = Widget(); (extra verbose). Widget w; (if your object has a default constructor, which it might not, who knows).
The behavior CHANGES depending on whether your Widget has an explicit constructor, a default constructor, a deleted constructor, or is aggregate-initializable. Each combination produces a different flavor of chaos.
--
So you've successfully constructed an object. Now let's talk about copy elision, where the language specification essentially shrugs and says "the compiler might copy your object, or it might not, we're not going to tell you."
Widget makeWidget() {
Widget w;
return w; // Does this copy? Maybe! Does it move? Perhaps! Does it do neither? Could be!
}
Pre-C++17, this was pure voodoo. The compiler was allowed to elide the copy, but not required to. So your carefully crafted copy constructor might run, or it might not. Your code's behavior was non-deterministic."But we have move semantics now!" Return Value Optimization (RVO) and Named Return Value Optimization (NRVO) are not guaranteed, depend on compiler optimization levels, and can be foiled by doing things as innocent as having multiple return statements or returning different local variables.
Widget makeWidget(bool flag) {
Widget w1;
Widget w2;
return flag ? w1 : w2; // NRVO has left the chat
}
Suddenly your moves matter again. Or do they? Did the compiler decide to be helpful today? Who knows! It's a surprise every time you change optimization flags!--
C++11 blessed us with auto, the keyword that promises to save us from typing out std::vector<std::map<std::string, std::unique_ptr<Widget>>>::iterator for the ten thousandth time. Most of the time, auto works fine. But it has opinions. Strong opinions. About const-ness and references that it won't tell you about until runtime when everything explodes.
std::vector<bool> v = {true, false};
auto x = v[0]; // x is not bool. x is std::vector<bool>::reference, a proxy object
x = false;
// v[0] is now... wait, what? Did that work? Maybe! If x hasn't been destroyed!
const std::string& getString();
auto s = getString(); // s is std::string (copy made), NOT const std::string&
You wanted a reference? Too bad! Auto decays it to a value. You need auto& or const auto& or auto&& (universal reference! another can of worms!) depending on your use case. The simple keyword auto has spawned a cottage industry of blog posts explaining when you need auto, auto&, const auto&, auto&&, decltype(auto), and the utterly cursed auto*.By the way, C auto now means the same as C++11 auto.
Last time I checked, even SpaceX uses C to send astronauts to space...
https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-and-c99
https://devblogs.microsoft.com/cppblog/c11-atomics-in-visual-studio-2022-version-17-5-preview-2/
https://learn.microsoft.com/en-us/cpp/c-runtime-library/compatibility?view=msvc-170
And the new guidelines regarding the use of unsafe languages at Microsoft, I wouldn't bet waiting that it will ever happen, even after 2040.
The box under the TV is a tiny part of the picture that makes Microsoft Games Studios and related subsidiaries.
https://en.wikipedia.org/wiki/List_of_Microsoft_Gaming_studios
If Microsoft really gets pissed due to SteamOS and Proton, there are quite a few titles Valve will be missing on.
At first I read Radeberger, a German beer brand.
People really need to stop acting like a garbage collector is some sort of cosmic horror that automatically takes you back to 1980s performance or something. The cases where they are unsuitable are a minority, and a rather small one at that. If you happen to live in that minority, great, but it'd be helpful if those of you in that minority would speak as if you are in the small minority and not propagate the crazy idea that garbage collection comes with massive "performance penalties" unconditionally. They come with conditions, and rather tight conditions nowadays.
Maybe; I sometimes write non-hobbyist non-performance-critical code in C.
I'm actually planning a new product for 2026 that might be done in C (the current iteration of that product line is in Go, the previous iteration was in Python).
I've few qualms about writing the server in C.
Bad Unicode support. Lack of cross platform system libraries. Needing to deal with CMake / autotools / whatever. Poor error handling. No built in string, list or map types. No generics. Nullability. No sum types. No option, tuples or multi returns. Generally worse IDE support than a lot of languages. No good 3rd party package ecosystem. The modern idiocy of header files. Memory bugs. Debugging memory corruption bugs. …
I mean, yeah other than all those problems, C is a great little language.
You make some good, if oft-repeated, points; but for my product:
1. Bad Unicode support - I'm not sure what I will use this for; glyphs won't be handled by a server program and storage/search of UTF8/codepoints will be handled by the data store (PostgreSQL, if you must know).
2. CMake/autotools/etc - low list of 3rd party dependencies, so a plain Makefile works.
3. Worse IDE support than a lot of languages - not sure what you mean by this. C has LSP support, like every other language. I haven't noticed C support in editors to be worse than other languages.
4. No 3rd party package ecosystem - That's fine, I'm not pulling in many 3rd party packages, so those that are pulled in can be handled with the Makefile and manual updates.
5. The modern idiocy of header files - this confuses me; there is still no good alternative to header files to support exporting to a common ABI. Functions, written in C, will be callable from any other language because header files are automatically handled by swig for FFI.[1]
6. Memory bugs + debugging them - thankfully, using valgrind, then sanitisers in my build/test step makes this a very low priority for me. Not that bugs don't slip through, but single-exit error handling using goto's and cleanups make these kinds of bugs rare. Not impossible, but rare. Having the test steps include valgrind, then various sanitisers reduces the odds even more.
For the rest, yeah, nice to have "No built in string, list or map types. No generics. Nullability. No sum types. No option, tuples or multi returns. ", but those are optional to getting a product out. If C had them I'd use them, but I'm not exactly helpless without them.
The downside of writing a product in C, in 2025, isn't in your list above.
========================================
[1] One of my two main reasons for switching to C is because the product was so useful to paying clients that they'd like more functionality, which includes "use their language of choice to interact with the product.". Thus far I've hacked in solutions depending on which client wanted what, but there's limits to the hacked-in solutions.
IOW, "easily extendable by clients using their language of choice" is a hard product requirement. If it wasn't a hard requirement they can continue using the existing product.
They're oft repeated because they're real problems.
> a plain Makefile works.
> C has LSP support, like every other language. I haven't noticed C support in editors to be worse than other languages.
Makefiles aren't supported well by clion or visual studio. LSP requires a compile-commands list to be able to work - which is a pita to export from makefiles. XCode and visual studio both require their own build systems. Etc etc. Its a mess.
Even if you set up LSP properly, debugging can still be a PITA. Most of the time, it doesn't "just work" like in many other languages.
In comparison, all Go projects look the same and all tooling understands them. Same for C#, Rust, Typescript, Zig, and many others.
> 5. The modern idiocy of header files - this confuses me; there is still no good alternative to header files to support exporting to a common ABI.
Other languages don't need header files, and yet they manage to export public interfaces just fine. Header files only exist because computers had tiny amounts of RAM in the 70s, and they couldn't keep everything in memory while compiling. The fact we keep them around in 2025 boggles my mind.
Header files create 2 problems:
1. You have to write them and keep them up to date as your function signatures change, which is pure overhead.
2. They slow down compilation, because the compiler has to re-parse your headers for every codegen unit. Yes, PCH exists - but its platform specific and complicated to set up yourself. You can use unity builds instead, but thats fiddly and it can cause other headaches.
> The downside of writing a product in C, in 2025, isn't in your list above.
What would you say the downsides of writing a product in C in 2025 are?
> One of my two main reasons for switching to C is because the product was so useful to paying clients that they'd like more functionality, which includes "use their language of choice to interact with the product."
Yeah; I agree that this is one area where C shines. I really wish we had better ways to do FFI than C ABI compatibility everywhere. Rust, Swift, Zig, C++ and others can of course all compile to static libraries that look indistinguishable from C object files. But if you're using those languages, writing a C API is another step. If you're already working in C, I agree that its much easier to write these APIs and much easier to keep them up to date as your code changes.
I dunno what IDE support I might need - once the Makefile is written I'm not going to be constantly adding and removing packages on a frequent basis.
As for IDE's, I am not using Clion, Visual Studio or XCode. Vim, Emacs and VSCode work fine with C projects, even when debugging interactively.
> What would you say the downsides of writing a product in C in 2025 are?
Slower initial development compared to HLL like Go, Python, etc. Well, it's slow if you want to avoid the major classes of bugs, anyway. You can go fast in C, but:
a) It's still not going to be as high-initial-velocity as (for example) Python, Java or C#
and
b) You're probably going to have a lot more bugs.
My standard approach to avoiding many of the pitfalls in using C (pitfalls which are also applicable to C++) is to use a convention that makes it easier to avoid most logic bugs (which, in the process avoids most memory bugs too). This convention does require a little more upfront design, but a lot more code.
So, yeah, I'll go slightly slower; this is not a significant enough factor to make anyone consider switching languages.
> Other languages don't need header files, and yet they manage to export public interfaces just fine.
Only for the isolated ecosystem of that language. C header files can be used to automatically perform the FFI for every single mainstream language.
So, sure, other languages can an export an interface to a file, but that interface probably can't be used for FFI, and in the rare cases where it can, it can't be automatically used.
C headers can, and are, used to automatically generated bindings for other languages.
I'm pretty sure VSCode still needs a compile commands file to do proper syntax highlighting. Again, difficult and annoying to generate automatically with makefiles.
To be clear, I'm not saying you can't set it all up. I'm just saying the IDE experience writing C is worse - obviously worse - than the experience programming in other modern languages.
> I'll go slightly slower; this is not a significant enough factor to make anyone consider switching languages.
Well, I can disprove that right now. I'm anyone. And the poor velocity writing C is a big reason I don't use it. Its just so much work to do simple things - like validate a UTF8 string. Or make a resizable array. Or write a function which can return either a value or an error.
In C, I feel like I'm a human compiler doing a bunch of things I want my compiler to do for me. C is slower to write and easier to mess up compared to more modern languages like Zig or Rust.
I did a side by side test a few years ago, implementing the same program in a variety of languages. The C code was about 2x as large as the same program in typescript, but it ran much faster. It was also much harder to debug. Zig and Rust were half the size of my C code and just as performant!
I don't see any reason to spend more work to achieve the same - or a worse - result.
> C header files can be used to automatically perform the FFI for every single mainstream language.
Every single one? I roll to doubt. Does the javascript ecosystem parse C header files for FFI? Does Java? C#? Rust? I know LuaJIT and Zig do, but I'm not sure about any others.
If C headers were only used for FFI, I wouldn't mind them so much. But they're also needed within a C program. And the semantics are idiosyncratic and weird. Want to inline something? It probably has to go in the header file as a static method. Want a global? Declare it in the header as an extern, and put it in exactly 1 C file. Structs? Did you want to declare the whole type or make it opaque? Because those are different. Functions can have different type signatures in header files and C files, but only in certain ways. And so on. So many weird, stupid rules you need to learn.
And by the time you've done all that, you're almost certainly leaving performance on the table as a result of not compiling the whole program in a single codegen unit.
Its 2025. Why waste your limited time on this earth acting as a human compiler? The computer can do a way better job of it.
TBH, I can't remember when I set VSCode up, or even if I did (may have just used a plugin and called it a day). ISTR something about clang, but it all just works so I didn't go into it to maintain
> To be clear, I'm not saying you can't set it all up. I'm just saying the IDE experience writing C is worse - obviously worse - than the experience programming in other modern languages.
If it is worse than Go, Java or C# in VSCode, I honestly did not notice, having used VSCode with all those languages.
> I don't see any reason to spend more work to achieve the same - or a worse - result.
Thankfully, the extra effort is very small, so I don't care because the extra payoff is so large.
> Every single one? I roll to doubt. Does the javascript ecosystem parse C header files for FFI? Does Java? C#? Rust? I know LuaJIT and Zig do, but I'm not sure about any others.
Okay, maybe not every single one, but Javascript (Node.js) and Java are definitely supported in Swig. C# supports native calling by default anyway so no swig support necessary, and Rust already can FFI into C code.
So, from your list, everything is supported.
> Its 2025. Why waste your limited time on this earth acting as a human compiler?
I'm not acting as a human compiler. Missing some optional features doesn't make me a human compiler (what a strange take, though. You sound personally aggrieved that someone who has delivered using Java, C#, Go, Python and more can be productive in C with only a small difference in velocity).
More language features rarely translate into more successful products. The most successful products, in terms of deployment numbers, were built with the least exciting technologies - plain Java prior to 2015, etc.
Why are you not worried about becoming the next Cloudbleed? Do you believe you have superhuman programming abilities?
The odds are just too low.
> Do you believe you have superhuman programming abilities?
I do not believe I have superhuman abilities.
I have never seen a language with a better ffi into C than C.
What massive, maintained set is that? Base Unix is tiny, and any serious programming ecosystem has good alternatives for all of it.
Quite a few OSes don't fit that rule.
About 80% of the OS APIs are behind JNI calls, when using the NDK.
iOS, iPadOS, watchOS, the large majority of userspace APIs is based on Objective-C, or Swift, bare bones C is only available for the POSIX leftovers.
You need to call the Objective-C runtime APIs for anything useful as an app that Apple would approve.
For the Plan 9 geeks, Inferno, OS APIs are exposed via Limbo.
For folks that still find mainframes and micros cool, IBM i, IBM z/OS, Unisys ClearPath MCP, Unisys OS 2200.
For retrogaming folks, most 8 and 16 bit home computers.
In a system programming language?
It is a matter to have an open mindset.
Eventually system languages with manual memory management will be done history in agentic driven OSes.
The optimisatios needed to improve such scenarions, are akin to a poor man's tracing GC implementation.
https://gchandbook.org/contents.html
It would help if all naysayers had their CS skills up to date.
I have been working in GC languages for the last 25 years. The GC has been a performance problem for me... once. The modal experience for developers is probably zero. Once or twice is not that uncommon. But you shouldn't bend your entire implementation stack choice over "once or twice a career" outcomes.
This is not the only experience for developers, and there are those whose careers are concentrated in the places where it matters... databases, 100%-utilization network code, hardware drivers. But for 99% of the programs out there, whatever language they are implemented in, GC is not an important performance consideration. For the vast bulk of those programs, there is a much larger performance consideration in it that could be turned up in 5 minutes with a profiler and nobody has even bothered to do that and squeeze out the accidentally quadratic code because even that doesn't matter to them, let alone GC delays.
This is the "system programmer's" equivalent of the web dev's "I need a web framework that can push 2,000,000 requests per second" and then choosing the framework that can push 2,001,000 rps over the one that can push 2,000,000 because fast... when the code they are actually writing for the work they are actually doing can barely push 100 rps. Even game engines nowadays have rather quite a lot of GC in them. Even in a system programming language, and even in a program that is going to experience a great deal of load, you are going to have to budget some non-trivial optimization time to your own code before GC is your biggest problem, because the odds that you wrote something slower than the GC without realizing it is pretty high.
Of course, but how many developers choose C _because_ it does not have a GC vs developers who choose C# but then work around it with manual memory management and unsafe pointers? ....... It's > 1000 to 1
There are even new languages like C3, Odin, Zig or Jai that have a No-GC-mindset in the design. So why you people insist that deliberately unsafe languages suddenly need a GC? There a other new languages WITH a GC in mind. Like Go. Or pick Rust - no GC but still memory safe. So what's the problem again? Just pick the language you think fits best for a project.
lol .. reality disagrees with you.
On page 3 they broadly conclude that if you use FIVE TIMES as much memory as your program would if managed manually, you get a 9% performance hit. If you only use DOUBLE, you get as much as a 70% hit.
Further on, there are comprehensive details on the tradeoffs between style of GC vs memory consumption vs performance.
---
Moving a value from DRAM into a CPU register is an expensive operation, both in terms of latency, and power consumption. Much of the code out in the "real world" is now written in garbage collected languages. Our datacenters are extremely power hungry (as much as 2% of total power in the US is consumed by datacenters), and becoming more so every day. The conclusion here is that garbage collection is fucking expensive, in real-world terms, and we need to stop perpetuating the idea that it's not.
> We introduce a novel experimental methodology that lets us quan- tify the performance of precise garbage collection versus explicit memory management. Our system allows us to treat unaltered Java programs as if they used explicit memory management by relying on oracles to insert calls to free. These oracles are generated from profile information gathered in earlier application runs.
A CLI tool (which most POSIX tools are) would pick throughput over latency any time.
In the best cases, you’re losing a huge amount of performance vs. an equivalent non-GC system. In the worst, it affects interactive UI performance with multi-second stalls (a suitably modern GC shouldn’t do this, though).
If I had a dollar for every time somebody repeated this without real-world benchmarks to back it up...
Basically java gc is a solution to a problem that shouldn't exist.
But you're never going to encounter a C++ program that does that, since it makes no sense.
I once worked on a python program that was transpiled to C++, and literally every variable was heap allocated (because that's what python does). It was still on the order of 200x faster than python IIRC.
This is beautiful!
C23 didn't introduce it, it's still a GCC extension that needs to be spelled as [[gnu::cleanup()]] https://godbolt.org/z/Gsz9hs7TE
https://en.cppreference.com/w/c/language/attributes.html
https://en.cppreference.com/w/cpp/language/attributes.html
https://gcc.gnu.org/onlinedocs/gcc/Attributes.html
https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#index-cleanup-variable-attribute
Discussion:
(These features are still essentially unsafe: the unique pointer implementation still permits UAF, for example, because nothing prevents another thread from holding the pointer and failing to observe that it has been freed.)
Macros are simply a fact of life in any decent-sized C codebase. The Linux kernel has some good guidance to try to keep it from getting out of hand but it is just something you have to learn to deal with.
Discussed here https://news.ycombinator.com/item?id=22191790
C++’s shared pointer has the same problem; Rust avoids it by having two types (Rc and Arc) that the developer can select from (and which the compiler will prevent you from using unsafely).
It doesn't. C++'s shared pointers use atomics, just like Rust's Arc does. There's no good reason (unless you have some very exotic requirements, into which I won't get into here) to implement shared pointers with mutexes. The implementation in the blog post here is just suboptimal.
(But it's true that C++ doesn't have Rust's equivalent of Rc, which means that if you just need a reference counted pointer then using std::shared_ptr is not a zero cost abstraction.)
But I suppose we're wasting time on useless nitpicking. So, fair enough.
Edit: in other words C++ could provide an equivalent of Rc, but we’d see no end of people complaining when they shoot themselves in the foot with it.
(This is what “zero cost abstraction” means: it doesn’t mean no cost, just that the abstraction’s cost is no greater than the semantically equivalent version written by the user. So both Arc and shared_ptr are zero-cost in a MT setting, but only Rust has a zero-cost abstraction in a single-threaded setting.)
Simply put, just as a `unique_ptr` (`Box`) is an entirely different abstraction than `shared_ptr` (`Arc`), an `Rc` is also an entirely different abstraction than `Arc`, and C++ simply happens to completely lack `Rc` (at least in the standard; Boost of course has one). But if it had one you could use it with exactly the same cost as in Rust, you'd just have to manually make sure to not use it across threads (which indeed is easier said than done, which is why it's not in the standard), exactly the same as if you'd manually maintain the reference count without the nice(er) abstraction. Hence "zero cost abstraction".
IMO "zero cost abstraction" just means "I have a slightly less vague idea of what this will compile to."
This seems workload dependent; I would expect a lot of workloads to be write-heavy or at least mixed, since copies imply writes to the shared_ptr's control block.
It does. It’s called a process.
Everyone chose convenience and micro-benchmarks by choosing threads instead.
I'd be interested to know what you are thinking.
The primary exotic thing I can imagine is an architecture lacking the ability to do atomic operations. But even in that case, C11 has atomic operations [1] built in. So worst case, the C library for the target architecture would likely boil down to mutex operations.
You can't create userspace locks which is a bummer, but the OS has the capability of enforcing locks. That's basically how early locking worked.
The main thing needed to make a correct lock is interrupt protection. Something every OS has.
To go fast, you need atomic operations. It especially becomes important if you are dealing with multiple cores. However, for a single core system atomics aren't needed for the OS to create locks.
Nit: while it's possible to implement one with just atomic reads and writes, it's generally not trivial/efficient/ergonomic to do so without an atomic composite read-write operation, like a compare-and-swap.
Yes. Also, almost every platform I know that supports multi threading and atomics doesn’t support atomics between /all/ possible masters. Consider a microcontroller with, say, two Arm cores (multithreaded, atomic-supporting) and a DMA engine.
// Not real MIPS, just what I've gleaned from a brief look at some docs
LOAD addr, register
ADD 1, register
STORE register, addr
The LOAD and STORE are atomic, but the `ADD` happens out of band.That's a problem if any sort of interrupt happens (if you are multi-threading then a possibility). If it happens at the load, then a separate thread can update "addr" which mean the later STORE will stomp on what's there.
x86 and ARM can do
ADD 1, addr
as well as other instructions like "compare and swap" LOAD addr, register
MOV register, register2
ADD 1, register2
COMPARE_AND_SWAP addr, register, register2
if (cas_failed) { try again } loop: LL r2, (r1)
ADD r3, r2, 1
SC r3, (r1)
BEQ r3, 0, loop
NOPSure, cross-platform is desirable, if there's no cost involved, and mandatory if you actually need it, but it's a "nice to have" most of the time, not a "needs this".
As for mutex overheads, yep, that's annoying, but really, how annoying ? Modern CPUs are fast. Very very fast. Personally I'm far more likely to use an os_unfair_lock_t than a pthread_mutex_t (see the previous point) which minimizes the locking to a memory barrier, but even if locking were slow, I think I'd prefer safe.
Rust is, I'm sure, great. It's not something I'm personally interested in getting involved with, but it's not necessary for C (or even this extra header) to do everything that Rust can do, for it to be an improvement on what is available.
There's simply too much out there written in C to say "just use Rust, or Swift, or ..." - too many libraries, too many resources, too many tutorials, etc. You pays your money and takes your choice.
> We love its raw speed, its direct connection to the metal
If this is a strong motivating factor (versus, say, refactoring risk), then C’s lack of safe zero-cost abstractions is a valid concern.
For this use-case, you might not notice. ISTR, when examing the pthreads source code for some platform, that mutexes only do a context switch as a fallback, if the lock cannot be acquired.
So, for most use-cases of this header, you should not see any performance impact. You'll see some bloat, to be sure.
There really isn't. Speaking as someone who works in JVM-land, you really can avoid C all the time if you're willing to actually try.
It's an implementation detail. They could have used atomic load/store (since c11) to implement the increment/decrement.
TBH I'm not sure what a mutex buys you in this situation (reference counting)
Do you have a source for this? I couldn't find the implementation in TFA nor a link to safe_c.h
I'd much rather it didnt try to be zero-cost and it always used atomics...
(for reference, the person above is referring to what's described here: https://snf.github.io/2019/02/13/shared-ptr-optimization/)
The "language" is conventionally thought of as the sum of the effects given by the { compiler + runtime libraries }. The "language" often specifies features that are implemented exclusively in target libraries, for example. You're correct to say that they're not "language features" but the two domains share a single label like "C++20" / "C11" - so unless you're designing the toolchain it's not as significant a difference.
We're down to ~three compilers: gcc, clang, MSVC and three corresponding C++ libraries.
// The old way of manual reference counting
typedef struct {
MatchStore* store;
int ref_count;
pthread_mutex_t mutex;
} SharedStore;In any case, you could use the provided primitives to wrap the C11 mutex, or any other mutex.
With some clever #ifdef, you can probably have a single or multithreaded build switch at compile time which makes all the mutex stuff do nothing.
BTW don’t fight C for portability, it is unlikely you will win.
Nim is a language that compiles to C. So it is similar in principle to the "safe_c.h". We get power and speed of C, but in a safe and convenient language.
> It's finally, but for C
Nim has `finally` and `defer` statement that runs code at the end of scope, even if you raise.
> memory that automatically cleans itself up
Nim has ARC[1]:
"ARC is fully deterministic - the compiler automatically injects destructors when it deems that some variable is no longer needed. In this sense, it’s similar to C++ with its destructors (RAII)"
> automated reference counting
See above
> a type-safe, auto-growing vector.
Nim has sequences that are dynamically sized, type and bounds safe
> zero-cost, non-owning views
Nim has openarray, that is also "just a pointer and a length", unfortunately it's usage is limited to parameters. But there is also an experimental view types feature[2]
> explicit, type-safe result
Nim has `Option[T]`[3] in standard library
> self-documenting contracts (requires and ensures)
Nim's assert returns message on raise: `assert(foo > 0, "Foo must be positive")`
> safe, bounds-checked operations
Nim has bounds-checking enabled by default (can be disabled)
> The UNLIKELY() macro tells the compiler which branches are cold, adding zero overhead in hot paths.
Nim has likely / unlikely template[4]
------------------------------------------------------------
[1] https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc-in-nim.html
[2] https://nim-lang.org/docs/manual_experimental.html#view-types
But if you can stay out of MSVC world, awesome! You can do so much with a few preprocessor blocks in a header
Also my comment was a single sentence with a single fact so it can't be a rant.
> -mabi=name Generate code for the specified calling convention. [...] The default is to use the Microsoft ABI when targeting Microsoft Windows and the SysV ABI on all other systems.
> -mms-bitfields Enable/disable bit-field layout compatible with the native Microsoft Windows compiler. [...] This option is enabled by default for Microsoft Windows targets.
Doesn't this work in practice, due to bugs?
Github has several repositories named cgrep, but the first results are written in other languages than C (Haskell, Python, Typescript, Java, etc).
> In cgrep, parsing command-line options the old way is a breeding ground for CVEs and its bestiary. You have to remember to free the memory on every single exit path, difficult for the undisciplined.
No, no, no. Command line options that will exist the entire lifetime of the program are the quintessential case for not ever calling free() on them because it's a waste of time. There is absolutely no reason to spend processor cycles to carefully call free() on a bunch of individual resources when the program is about to exit and the OS will reclaim the entire process memory in one go much faster than your program can. You're creating complexity and making your program slower and there is literally no upside: this isn't a tradeoff, it's a bad practice.
Pros: preallocating one arena is likely faster than many smaller allocations.
Cons: preallocation is most effective if you can accurately predict usage for the arena; if you can't, then you either overshoot and allocate more memory than you need, or undershoot and have to reallocate which might be less performant than just allocating as-needed.
In short, if you're preallocating, I think decisions need to be made based on performance testing and the requirements of your program (is memory usage more important than speed?). If you aren't preallocating and just using arenas for to free in a group, then I'm going to say using an arena for stuff that is going to be freed by the OS at program exit is adding complexity for no benefit--it depends on your arena implementation (arenas aren't in the C standard to my knowledge).
In general, I'd be erring on the side of simplicity here and not using arenas for this by default--I'd only consider adding arenas if performance testing shows the program spending a lot of time in individual allocations. So I don't think a blanket recommendation for arenas is a good idea here.
EDIT: In case it's not obvious: note that I'm assuming that the reason you want to use an arena is to preallocate. If you're thinking that you're going to call free on the arena on program exit, that's just as pointless as calling free on a bunch of individual allocations on program exit. It MIGHT be faster, but doing pointless things faster is still not as good as not doing pointless things.
What makes you think that the program is going to exit anytime soon?
Arenas are useful if you want to execute a complex workflow and when it is over just blow away all the memory it accumulated.
An HTTP server is the textbook example; one arena per request, all memory freed when the request is completed, but there's many many more similar workflows in a running program.
Leaving it for program exit is a luxury only available to those writing toy programs.
The real problems start when you need to manage memory lifetimes across the whole program, not locally. Can you return `UniquePtr` from a function? Can you store a copy of `SharedPtr` somewhere without accidentally forgetting to increment the refcount? Who is responsible for managing the lifetimes of elements in intrusive linked lists? How do you know whether a method consumes a pointer argument or stores a copy to it somewhere?
I appreciate trying to write safer software, but we've always told people `#define xfree(p) do { free(p); p = NULL; } while (0)` is a bad pattern, and this post really feels like more of the same thing.
Yes: you can return structures by value in C (and also pass them by value).
> Can you store a copy of `SharedPtr` somewhere without accidentally forgetting to increment the refcount?
No, this you can't do.
Have we? Why?
The benefit of these types is that they're a pair of pointer+size, instead of just a bare pointer without a known size.
In fact I don't see anything here from Rust that isn't also in C++. They talk about Result and say "Inspired by Rust, Result forces you to handle errors explicitly by returning a type that is either a success value or an error value", but actually, unlike Rust, nothing enforces that you don't just incorrectly use value without checking status first.
It's just some macros the author likes. And strangely presented—why are the LIKELY/UNLIKELY macros thrown in with the CLEANUP one in that first code snippet? That non sequitur is part of what gives me an LLM-written vibe.
For the first item on reference counting, batched memory management is a possible alternative that still fits the C style. The use of something like an arena allocator approximates a memory lifetime, which can be a powerful safety tool. When you free the allocator, all pages are freed at once. Not only is this less error prone, but it can decrease performance. There’s no need to allocate and free each reference counted pointer, nor store reference counts, when one can free the entire allocator after argument parsing is done.
This also decreases fallible error handling: The callee doesn’t need to free anything because the allocator is owned by the caller.
Of course, the use of allocators does not make sense in every setting, but for common lifetimes such as: once per frame, the length of a specific algorithm, or even application scope, it’s an awesome tool!
I don't see it that way, mostly because ADTs don't require automatic destructors or GC, etc, but also because I never considered a unique/shared pointer type to be an abstract data type
> When you free the allocator, all pages are freed at once. Not only is this less error prone, but it can decrease performance.
How does it decrease performance? My experience with arenas is that they increase performance at the cost of a little extra memory usage.
Like, if I was stuck in a C codebase today, [[cleanup]] is great -- I've used this in the past in a C-only shop. But vectors, unique_ptrs, and (awful) shared_ptrs? Just use C++.
// The Old Way (don't do this)
char* include_pattern = NULL;
if (optarg) {
include_pattern = strdup(optarg);
}
// ...200 lines later...
if (some_error) {
if (include_pattern) free(include_pattern); // Did I free it? Did I??
return 1;
Nope! // ...200 lines later...
// common return block
out:
free(include_pattern); // free(NULL) allowed since before 1989 ANSI C
return result;You can do cleanup handling that integrates with your exception handling library by using pairs of macros inspired by the POSIX pthread_cleanup_push stuff.
#define cleanup_push(fn, type, ptr, init) { \
cleanup_node_t node; \
type ptr = init; do cleanup_push_api(&node, fn, ptr) while (0)
#define cleanup_pop(ptr) cleanup_pop_api(ptr) \
}
cleanup_push_api places a cleanup node into the exception stack. This is allocated on the stack: the node object. If an exception goes off, that node will be seen by the exception handling which will call fn(ptr).The cleanup_pop_api call removes the top node, doing a sanity check that the ptr in the node is the same as ptr. It calls fn(ptr).
The fact that cleanup_push leaves an open curly brace closed by cleanup_pop catches some balancing errors at compile time.
The POSIX pthread_cleanup_pop has an extra boolean parameter indicating whether to do the cleanup call or not. That's sometimes useful when the cleanup is only something done in the case of an abort. E.g. suppose tha the "cleanup" routine is rollback_database_transaction. We don't want that in the happy case; in the happy case we call commit_database_transaction.
This is cute, but also I'm baffled as to why you would want to use macros to emulate c++. Nothing is stopping you from writing c-like c++ if that's what you like style wise.
It's choosing which features are allowed in.
Though yes, you should probably just write C-like C++ at that point, and the result sum types used made me chuckle in that regard because they were added with C++17. This person REALLY wants modern CPP features..
I like the power of destructors (auto cleanup) and templates (generic containers). But I also want a language that I can parse. Like, at all.
C is pretty easy to parse. Quite a few annoying corner cases, some context sensitive stuff, but still pretty workable. C++ on the other hand? It’s mostly pick a frontend or the highway.
You'll just have to get used to the C++ community screaming at you that it's the wrong way to write C++ and that you should just use Go or Zig instead
[1] https://doc.rust-lang.org/beta/rustc/platform-support.html
However most of the embedded world uses ARM chips and they are Tier 2 like thumbv6m and thumbv7em (there are still odd ones like 8051 or AVR or m68k, many of them lack a good C++ compiler already). They are guaranteed to be built and at the release time the tests still run for them.
Quite frankly I'm not sure why you wouldn't given that most are using GCC on common architectures. The chip vendor doesn't have to do any work unless they are working on an obscure architecture.
C’s simplicity can be frustrating, but it’s an extremely hackable language thanks to that simplicity. Once you opt in to C++, even nominally, you lose that.
(Agree on your other points for what it's worth.)
[1] https://godbolt.org/z/hvj9vcncG
[2] https://github.com/ludocode/onramp
[3] https://github.com/fuhsnn/slimcc