Writing New System Software
borud.no
borud.no
> But I have to say that I’ve noticed a shift in culture away from having a solid basis in knowledge to fashions and dogma playing a bigger role in the design choices people make.
There have long been technologists who just wanna glue stuff together, go home at the end of the day and collect their pay packet. 20 years ago, I worked as an engineer in an office where a guy from IT support did some training at night school and went off to become a SAP consultant. SAP was fashionable at the time and I’m sure it had its own dogma. He probably never once thought about memory allocation or the efficiency of his code.
It was absolutely not the career path for me, but there’s nothing remotely wrong with his choices, and it’s certainly not a new thing. Just different career paths, and different interests.
Now - get off my lawn!
tl;dr: C and C++ have cooties, use literally anything else.
Nothing to see here.
https://docs.microsoft.com/en-us/cpp/build/projects-and-buil...
> IBM Open XL C/C++ for AIX and XL C/C++ for AIX documentation library
https://www.ibm.com/support/pages/ibm-open-xl-cc-aix-and-xl-...
The author's main argument is that other languages that exclude categories of bugs that keep on popping up in the C/C++ world are good enough at this point to be used for essentially all system programming stuff.
Apple, Google, Microsoft, etc. employ the best people that money can buy them. And they've each started actively discouraging the use of C/C++ for new stuff. Google actually created Go for this reason and they are now also using a lot of Rust lately. Apple created Swift to get rid of object C. And MS has been trying to move to C# (which they created) for the last two decades to put a stop to all the embarrassing issues they had with their native stuff. They too seem to like Rust lately. And obviously they each have to own up in public regularly about cases where, "oops, we did it again" despite having spent the last few decades to avoid having to do that so regularly. The usual suspects are bits of C/C++ doing things wrong with memory and bounds checking because their programmers made a mistake. It seems leaving this to humans to do "right" just is not good enough at this point.
The article gets a bit messy with a rant on stateful vs. stateless and a few other things. But the main argument with that seems to boil down to the notion that depending on a lot of stuff like databases, memory caches, etc. that are often implemented using C/C++ is neither fast (because of network latency) nor safe (because of the above mentioned bugs and security issues). It's true and it's why alternatives to these infrastructure components written in other languages are a thing. They are plenty fast and in so far they are not, the network latency hides most of the issues to the point where you'd not notice much difference in terms of e.g. throughput or latency at the price of maybe slightly more CPU usage on servers that are mostly running nowhere even close to 100% CPU usage. Besides, adding more CPUs is cheap. Dealing with security problems is not.
And of course using Rust for things that really need to be fast is a thing. There's growing amount of projects that are about creating drop in replacements for C/C++ things that have existed for a very long time where the goal is to actually improve their performance and safety by re-implementing them in Rust. It's the argument the article does not make. But it seems that with Rust, you can have your cake and eat it in terms of performance. So, why bother with C/C++ for new stuff? Why risk security bugs when the main argument of better performance simply does not hold true anymore? It's a solid argument. But I agree the article does a weak job of making it.
But even Apple still notes that you should not write audio processing code in Swift, and if you use Objective C for this purpose, to avoid the Objective parts of it.
I'm constantly reminded of the NPC in Half Life 1 who says something like "Take me with you, I'm the one man who knows everything!"
I know many programmers who think their one way of writing C++ will not cause any issues. None of them are 100% correct as I can always find a bug that a higher level language could have prevented and I'm not an "expert" in C++. I just know enough to be dangerous.
If you look at something like `apt-cache show podman | grep ^Built-Using:` (Output: https://paste.debian.net/plain/1225449) on Debian 11, you will see why. Imagine a few of those components shared between tens of packages, and security problems discovered im some. It's got to be any package maintainer's worst nightmare.
It doesn't. But it gives people a real alternative to doing what you describe! And only when a real alternative to something bad exists, one can reasonably demand that people stop doing the bad thing (Debian does, so does Fedora and a few other distros I believe).
1. Polymorphism 2. Specialization of code and data structures
Both aspects only go well together with global compilation which basically enforces static linking, unless you either substantially improve the ABI or introduce some artificial language limits regarding composition.
Personally, I think that it should be possible to extend the C-ABI with some meaningful form of specialization: You effectively only need offsets and sizes in order to specialize code. But I don't have a prototype to explore this idea further.
Specialization eliminates nearly all overhead of polymorphism so there's no disincentive against using it.
Is this really a problem in most modern package management systems and build systems? I think Nix is a great example of handling this gracefully by just tracking the inputs and outputs of each compilation and sandboxing the compilation so we can guarantee the build definition includes all specified deps. This is something debian can build on top of apt.
Imagine a Debian archive (that is, a few tens of thousands of packages with source, build recipies and binaries for multiple architectures available) with only static linking on the table. You got a few hundreds to thousands of those packages installed on typical, real-world Debian machines. Then, a central component like zlib or OpenSSL gets an important patch. This triggers hundreds, if not tens of thousands (transitive library .so dependencies are a thing after all, and you want to have all your supported arches covered) of rebuilds across the archive. All users will have to re-download and re-install all resulting upgraded packages they have installed.
With increased sharing of source code (i.e., when following "don't build your own crypto!" and other generally useful mantras by re-using existing codebases) and only static linking on the table, this kind of problems gets really, really bad really quickly for large-ish software archives. Kinda like with log4j in the Java ecosystem, where each app is used and event expected to dragging all its runtime dependencies along on its own.
In the dylib world we are living in right now however, users get a single updated package, and upon the next restart of an application linking against it, they're immunized against the bug. The reduction of churn is nigh-unfathomably massive.
If your systems generally only run a single application, and there's a dedicated ops team at the top (or bottom - it's a matter of perspective, I guess :)) of a monorepo world with each dependency fully vendored in, and an "intelligent" build and deployment system atop it all that will take care of gracefully upgrading everything everywhere upon such routine patching, I can see how statically linking everything reduces the amount of potential pain points upon deploying stuff. For the significant portion that makes up the rest of the world, they generally don't have such luxuries, and will be a lot worse off. Especially established and wonderful Free Software communities like Debian or Fedora. I think that would be a huge sacrifice to make.
Package managers already have ways to do this since no programs are 100% dynamicly or statically linked. Even Golang has dynamic linking (glibc recently).
> but I really would like to make everyone think about whether we should...
Why not? This has the major benefits of making distributed rebuilds, caching of builds, etc much easier. Nix and Bazel have been very successful with this.
> Imagine a Debian archive (that is, a few tens of thousands of packages with source, build recipies and binaries for multiple architectures available) with only static linking on the table. You got a few hundreds to thousands of those packages installed on typical, real-world Debian machines. Then, a central component like zlib or OpenSSL gets an important patch. This triggers hundreds, if not tens of thousands (transitive library .so dependencies are a thing after all, and you want to have all your supported arches covered) of rebuilds across the archive. All users will have to re-download and re-install all resulting upgraded packages they have installed.
The story isn't as bad as you might think. I'd wager that most programs do not dynamically link anything other than glibc: https://drewdevault.com/dynlib
> With increased sharing of source code ... and only static linking on the table, this kind of problems gets really, really bad really quickly for large-ish software archives. Kinda like with log4j in the Java ecosystem, where each app is used and event expected to dragging all its runtime dependencies along on its own.
With LTOs it's possible to remove the 99% of the code that isn't used from these libraries. It's not as bad as one might think and, if you define your libraries in smaller scopes, you can include only a few .so files.
> In the dylib world we are living in right now however, users get a single updated package, and upon the next restart of an application linking against it, they're immunized against the bug
This is the positive case but the unfortunate negative is that a majority of the time the program breaks. To save 30MB to 100MB of network traffic we introduce the possibility of an breaking all bins. An overwhelming majority of my binaries use less than 10% of the symbols of their dynamic libraries. Also, if things are statically linked, we can run automated testing on binaries to catch errors before deploying package updates. Maybe a security fix breaks your favorite app? Useful info to know.
> of a monorepo world with each dependency fully vendored in, and an "intelligent" build and deployment system atop it all that will take care of gracefully upgrading everything
That's sort of the description of what OS distros do IMO.
> For the significant portion that makes up the rest of the world, they generally don't have such luxuries, and will be a lot worse off. Especially established and wonderful Free Software communities like Debian or Fedora. I think that would be a huge sacrifice to make.
IMO having a system like what Nix has to do hermetic builds would improve debian and other OSs as they could build, test, and reproduce builds more easily. This is important for security.
1. I’m amazed that you call Java ugly and verbose but recommend C++. Apart from files not needing to be classes in C++, they feel similarly verbose to me with C++ slightly winning on ugliness.
2. I basically don’t believe that memory management is not hard. The reason is that plenty of programs written in ‘modern’ or ‘safe’ C++ in the style you recommend continue to have plenty of memory unsafety based bugs, crashes, and vulnerabilities.
P.S. Most async systems try to keep things on a single thread. Their main purpose is allowing a higher level of parallelism than can be economically achieved with threads, and normal good async style is to have most things non-async and therefore single-threaded. On Linux, a lot of the purpose of the thread pool is doing system calls that are blocking. io_uring makes it less necessary but many people use kernels that are too old.
I've always found java much much more verbose. here's e.g. hello world in javafx:
package helloworld;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
here's a Qt version: #include <QtWidgets>
int main(int argc, char** argv) {
QApplication app{argc, argv};
QPushButton button{"Say 'Hello World'"};
button.setWindowTitle("Hi");
QObject::connect(&button, &QPushButton::clicked, [] {
puts("Hello world !");
});
button.setMinimumSize({300, 250});
button.show();
return app.exec();
}
notice for instance creating an object just with {} ; just that saves me I don't know how many hundreds of keystrokes. Also no need to ever `new` anything, just name the types directly. No need to repeat types à la StackPane root = new StackPane();
C++ would just be StackPane root;
or maybe auto root = StackPane{foo, bar};
observer pattern can be much less braindead thanks to the early existence of templates, and let's not even talk about https://docs.oracle.com/javase/8/docs/api/java/util/function...Quoting stackoverflow:
https://stackoverflow.com/questions/18400210/java-8-where-is-trifunction-and-kin-in-java-util-function-or-what-is-the-alt
> There are ready to use Consumer3..Consumer8, Function3..Function8, Predicate3..Predicate8 in reactor.function package of Reactor Addons library that is bundled with Spring Framework.like, come on...
same for Optional which gets some unrelated custom OptionalInt because there was no value type and adds boilerplate for nothing.
Since Java 10,
var root = new StackPane(); package dev.kronis.jfxdemo;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloApplication extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
var btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(event -> System.out.println("Hello World!"));
var root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
That's now 26 lines and 765 characters.Well, more like 18 lines of actual code, since imports will almost never be written manually and my IDE collapses them by default, and 505 characters.
For comparison, the code for Qt is 16 lines and 335 characters.
To me, those differences are not significant enough for them to matter all that much.
As for the whole TriFunction thing, i feel like most modern languages are bad in regards to handling functions and their parameters as well as their return values. The thing that i've found most annoying is having container/wrapper objects for function returns when you need to return more than one thing from a function, or alternatively have to pass in a mutable container object of sorts which muddies things (much like how on the DBMS side you have OUT parameters). In contrast, just look at how Go does things, you can return multiple values from any function!
Here's hoping that Java and other languages keep improving in the future and we get more tasteful syntactic sugar to cut out the unimportant and menial boilerplate code. In the mean time, Kotlin is also decent, look at the following function syntax wise:
override fun start(primaryStage: Stage) {
primaryStage.title = "Hello World!"
val btn = Button()
btn.text = "Say 'Hello World'"
btn.onAction = EventHandler { println("Hello World!") }
val root = StackPane()
root.children.add(btn)
primaryStage.scene = Scene(root, 300.0, 250.0)
primaryStage.show()
}
(purposefully excluded the @JvmStatic companion object here, since didn't bother much with the Java to Kotlin conversion, though IntelliJ IDEA is pretty good for that)If you try to write Java in C++ then probably the result is verbose. With some practice though, memory management needn't be hard - if memory management is hard that hints lack of organization.
Smaller, script-like programs where there is no place for large-scale organization, are a different story (especially in C and C-like C++) and GC'ed languaged might be more convenient.
See https://www.cvedetails.com/vulnerability-list/vendor_id-1224... for a list of RCE vulnerabilities. I see a bunch of memory management problems there. Also note that chrome is likely much better tested than some other C++ project, and lots of tools are used to search for potential vulnerabilities or invalid memory use as well as fuzzing and crash reports from a large install base. I don’t know how this compares with IntelliJ/Java (which I also don’t particularly like).
I would expect other C++ applications to have a higher defect rate than chrome due to less testing/tooling but also likely a lower attack surface area.
Except, maybe, that it takes a long time to modernize old code.
You may read in numerous places what modern C(+ code looks like. There is no need to guess.
Fuzzers and testing can detect bugs but I don't see how they can make a contribution to a reasonable software structure (I believe that bad structure is the prime cause of bugs).
I don’t mean this in a rude way, but I don’t care what you believe, because the thing I care about is evidence. If you have some actual evidence for your claim, I would be interested. (I also believe there can be improvements from better code structure in a rewrite and this can make up for a significant portion of the improvements from a ‘let’s rewrite it all in rust’ project).
I would be interested what is the evidence that you're talking about. If my experience doesn't speak to your experience, so be it.
Maybe I’m just missing the point though as I think I’m only writing obvious well-known things and you probably mean something more nuanced?
Emplace constructs copy of object in the container. You should be fine.
Just like JEE was originally written in Objective-C during the OpenSTEP days as the Distributed Objects Everywhere framework, and then ported to Java afterwards.
I like to use raii to handle normal pointers. For many programs memory leaks can just be handled by exit, too.
Sadly if the platform ABI is not ignored there is (usually? might be free on some platforms) overhead relative to a void* when passing one between functions.
Also there's the usual code size / compile time hazards of templates.
The overhead is not inherent, but is rather a consequence of C ABI choices inherited into C++ calling conventions. It is, though, real, and not easily fixed. E.g., RISC-V's ABI binding peobably suffers from the same overhead, not for any good reason, but just because the RISC-V crowd couldn't spare the attention to fix it.
What the hell is this referring to?
If you're talking about Embedded or Kernel Systems, writing a layer that can efficiently and securely multiplex and abstract the hardware requires a much different set of tools than the rest of the system. Here I disagree with the author.
Once you're no longer concerned with directly probing the hardware, it seems far more appropriate to worry about correctness, then complexity/performance. C/C++ are unlikely to be the best match to the needs of the system and the programmer. Here I agree with the author.
Any language can have compiler extensions.
Heck we used to do it in BASIC.
Sure, it is not as "complete" as standard libraries in Python, Go, etc. It is rather a different kind of beast than those standard libraries, not as high-level, more building blocks oriented.
In many (most?) cases, writing your own stdlib alternatives still makes a lot of sense.
The whole team needs to care about secure code to turn on checked iteration in release builds, or write their own wrappers if portability to compilers without such support is a concern.
Sources? I haven't heard of any company disallowing it, on the contrary, have heard of companies promoting its use.
edit: fixed formatting
anecdotally in the last ten years I haven't seen a single C++ codebase not using the stl, except arduino-level stuff
You seem to be implying that's a good thing. It's literally not.
Imagine if I created a programming language where every random string of characters was a valid program (cue the Perl jokes). Clearly this language permits even more freedom than C++, but this would be a nightmare for programming.
Constrained structure is essential to programming. Sometimes you need to beyond the constraints of a more common language, in which case C++ might be a good choice, but that's the exception not the rule.
, that it must be true that utility decreases with the unconstrained-ness of a language (and thus increases with more constraint).
However, this is not true. You only have to look to the other extreme to see there must be a middle ground. A language where there is only one valid program has no more utility than one where any string is a valid program.
Because this relationship of constraint and utility is clearly not simple, we can't use those extrema to judge if C++ is less useful because it gives so much control. There might be some "local extrema" where a language fits a niche. C++ might fill that niche, or it might not, but I think it needs a bit more of a nuanced consideration than "less constraint, bad".
The converse is actually the OP's argument, ie. that a language's utility increases with unconstrainedness. I merely showed that to be false, and argued that constraints are essential, but nowhere did I suggest that utility scales with the number of constraints.
I think it is.
At the point you rule out using available facilities of the language you might as well use something else.
If I recall correctly, Google's rationale regarding exceptions is that their legacy code is not exception-safe, and so they were faced with the choice of either rewriting critical parts of their legacy code to handle exceptions, or don't use them.
Also, their "no exceptions" rule only applied to work involving their legacy code.
I'm too lazy to find the source, but that bit of trivia was already discussed ad nauseum even in HN.
The morale of the story is that you should not mindlessly repeat any opinion without knowing the rationale and instead pulling appeals to authority to cover up the logical hole. That's how you end up contradicting even your source, just because you believed that's how the cool kids do things.
That's not the case, the no exceptions rule applies to legacy and non-legacy code: https://google.github.io/styleguide/cppguide.html#Exceptions.
> Also, their "no exceptions" rule only applied to work involving their legacy code.
The reasons for not using exceptions are outlined in this post: https://abseil.io/tips/76
absl::Status is basically exceptions without language sugar. Or, another way of looking at it, golang err in C++. Basically, non-local control flow is dangerous as it can't be evident to the programmer when something deep in the stack will bubble up an exception.
It is super annoying to deal with sometimes but there are a lot of amazing helper macros (yuck for other reasons) that exist. More details can be found looking through here: https://cs.opensource.google/search?q=ASSIGN_OR_RETURN&sq=
This is the crux of the error you're making. Exceptions are not about control flow. Exceptions are a transparent and clean way to handle exceptional events. Exceptions are not intended to, say, handle the status code of a HTTP requests. Exceptions are intended to handle exceptional and potentially unrecoverable errors in a safe and controlled manner, such as failing to allocate memory, regardless of where and how they pop up.
Therefore, suggesting classical C-style return codes or specialized monadic types to handle results as alternatives to exceptions completely misses the whole point of exceptions and, more importantly, all the classes of problems they are designed to eliminate.
I'd sum up this argument as: Exceptions are syntax sugar that allow callers of a function to ignore the error cases of something they are calling and depend on something that calls into them to handle the error case.
An example:
void HandleRequest(const Request &request, KV &kv) {
...
kv.set("a", 10);
...
}
In this example KV.set will raise an exception. The programmer implementing HandleRequest isn't directly exposed to this fact and so to them, and the reviewer, and future onlookers this code looks correct. Now lets say kv.set() throws an exception in production under a specific case. Maybe there were two people attempting to set the same value at the same time or a networking issue. Doing this in the context of a webserver might make sense as the webserver might handle exceptions as error codes but that's not the end-all-be-all. Suppose we refactored to something like this Something CreateSomething(....);
How do you know if this function will throw an exception? How do you find all of the possible exceptions that can be thrown? Statically you can't really. If instead you see absl::StatusOr<Something> CreateSomething(...);
You can tell for sure that the result has some error that needs to be handled. Your original code: kv.set("a", 10);
This code no longer compiles in an absl::Status world. Instead you'd need to do something like: CHECK_OK(kv.set("a", 10)) // this will panic
RETURN_IF_ERROR(kv.set("a", 10)) // Bubble up
From this we get:1. a stack trace since RETURN_IF_ERROR adds metadata about the call site.
2. Guarantee that code will not compile if errors are not handled.
3. Guarantee that future readers know that some very high level function could probably call into code that can produce an error you need to handle.
This matters much more if things like this are happening:
kv.beginTransaction();
kv.set(...);
kv.endTransaction();
This could be handled by destructors in this case but in other cases: otherService.startingWork();
kv.set(...);
otherService.doNextStep();
There are cases where destructors do not make sense. You do not always want to call `doNextStep()` as it would be 100% wrong in the case where we cannot set our value in our kv store. Contrived but I've run into these in real life services. If a developer sends me the above code snippet I might LGTM. If the developer instead sends me: otherService.startingWork();
if (!kv.set(...).ok()) { log("something went wrong"); }
otherService.doNextStep();
I'll be able to point to the exact problem with this code much more easily. Also if there's an outage and I need to read this code I can clearly see why this `something went wrong` in the logs correlates to incorrectly called doNextStep().I'm not saying that Status is perfect (I'm not 100% sold) but exceptions are a type of control flow in an abstract sense. The problem some people have with it is it's control flow you can't audit.
Exceptions are a way to ensure that failures are directed immediately to a place designated to deal with such a failure.
They are, in particular, not any sort of "syntax sugar", unlike "?" in certain other languages, or your StatusOr thing. A function that throws an exception does not, in any sense, return to its caller. It does not construct any sort of return value. It does not consult the stack to see where it came from and resume running there.
And, exceptions are totally auditable. There is never any hint of ambiguity or uncertainty about where an exception will take you.
You can be confident that if a function was thrown from, it was because it could not perform the requested action. And, you can be confident that if an exception was not thrown the function called satisfied whatever postconditions it promised.
So, you don't need to know if a function might throw. You may instead assume any function might throw. If it doesn't, then it has satisfied its documented postconditions. Your obligation is only to ensure that destructors clean up any intermediate state on the way out. These identical destructors get exercised every time through the code, so are exercised frequently.
Error-handling code at places where the error cannot actually be dealt with properly, that just tries to propagate the failure up the call chain, is typically not well tested, and often cannot even be triggered in testing.
You can even start by the new entries related to Rust.
https://source.android.com/setup/build/rust/building-rust-mo...
I have no need to read hype about Rust, regardless of where it might run. And, I have no desire to build Android apps, in any language.
The reference to Rust was just an example on how such members are also looking for alternatives, other big names are looking into Swift, C#, whatever.
Meanwhile clang crawls along on its support for newer ISO C++ features, as the biggest contributors now have their focus elsewhere.
What did you mean by this? I am not an LLVM expert or anything, but I find more-often-than-not most of the new features I read about that make its way into LLVM are requirements needed due to clang adding things from new C++ standards. These improvements then benefit other frontends like the one for Rust.
Well, for Apple, C++ is relevant only in the context of Metal shaders (a C++14 subset) and IO/DriverKit (an embedded C++ like subset), everything else is about Objective-C and Swift, with C++17 being good enough for whatever else they need.
Likewise on Google style, it is all about C++ guidelines at Googleplex, where even not all C++17 features are welcomed.
Everyone else seems more interested in C++ features for their own platforms rather than contributing to upstream (thanks license), so there you go.
If your goal was to use that poor example to support the idea that people stick with subsets of C++ because of reasons, that example failed to support the assertion. Thus it makes no sense to stick with a patently wrong observation just because it's easier to recall.
Getting back to the topic, as far as I know there are only two features of C++ which are up for debate regarding their adoption: exceptions, and template metaprogramming. The exception-handling debate only makes sense in very low-level applications and refactoring legacy exception-less code, which in practice is not anyone's case. The template metaprogramming debate typically boils down to YAGNI and the need to avoid resume-driven development. Nevertheless, both features are used extensively, whether directly or indirectly (see STL), and in general there is no reason to bother debating whether people should use it or not, unless you have very specific requirements in mind (I.e., avoid generating magical code in embedded applications, or in very high performance applications where you feel you need tight control over everything down to which instructions are generated).
Because there is no need to. C++ is vast and there is no point in exploring all possible ways of doing something once decent path exists.
Languages that try to build in some "house style" are my personal dystopia - such as early Java or current Go. Which does not say they ate not effective, just that I personally hate the philosophy.
Almost never seen that in practice. I always see people talking about it in forums, but in real-world gigs I don't know people who artificially restrict their codebase with braindead rules like that.
I have yet to see more than "print and bail out" in catch blocks. In embedded there is nobody who can read your cry for help and especially in fail-op systems this is just not an option.
Herbceptions are just not there yet and until then we help ourselves with things like "expected" for example.
In fact probably yet another reason why Apple and Google aren't in an hurry to improve clang to latest ISO, and other companies in clang ecosystem even less.
"I can appreciate the “macho factor” of being able to write fast software in C or C++ (or even Objective-C), but most people aren’t going to be able to do that"
I mean, C is probably the simplest tool to write fast software with, since there are so few hidden costs to know about. Like strdup does a malloc. What else?
In C++ you have to know implementation details of the standard lib and calling conventions to write fast code. Java, C# and Go the same but abit more.
For e.g. Haskell and Julia you need to know quite much to reason about what code will be generated and good luck doing that with complex code.
The simplicity is the hard part. Fishing for compliments by "pretending" it's so trivial to you shows immaturity.
The author definitely knows what he's talking about. Perhaps it's you who's doesn't?
Simpler in the amount of knowledge you need to have to write the fast program.
I mean just compare "C the programming language" with Stroustrup's "C++ the programming language". It took me years to understand even a fundamental thing as move semantics. When programming fast C++ you need to be able to see what is moved properly and optimized in the way you want. How classes are inlined into the code etc. What container do malloc on construction, what containers are cheap empty, and so on and on.
There is no such knowledge needed for C.
Looking at the whole statement I think that the argument is that algorithms are more important than language when it comes to speed which is correct (with a few exceptions).
I don't think it's so cut and dry.
What the software actually attempts to do probably also matters a lot for what "reasonably fast" is. Would i write a distributed file system, a web server, or a database engine, or some OS component in pure Python? Probably not (assuming CPython, something like PyPy might make it more viable). But at the same time, Go might be a decent option for any/all of those, despite generally being a bit slower than C/C++/Rust.
That said, Python, Lua or anything else that's often touted as a decent language despite being on the slower side could be a good choice for glue code (just look at machine learning, number crunching and other domains where Python is pretty common), maybe creating various scripts (e.g. for setup/automation of common tasks without having to do everything in Bash), or maybe developing CLIs/TUIs or libraries to act as front ends for more complex bits of other software.
What probably also matters a lot for system software is C interop, or just how well the software can integrate with the system libraries etc., unless we're talking about something that's fully statically linked. Of course, this doesn't necessarily have that much to do with speed, just another factor to consider.
Also, while on the topic of other things that matter, the speed of development is also one such thing - not everyone has excellent knowledge of the intricacies of working with lower level languages and frameworks for them. Surely system software shouldn't necessarily be restricted to a select few developers, so anything with fewer footguns could be worthy of consideration!
In short, use whatever works for you, but "reasonably fast software" isn't all too restrictive of a target so in the modern day most languages can indeed find their place in the grand scheme of things. There's very little preventing you from writing most of your software in Python and writing the 10% of performance intensive code in something else.
Of course, limitations apply - for example, writing GUI software with Electron will usually be done for ease of development rather than performance (just look at how much resources Microsoft allots to getting Visual Studio Code to perform well vs something like Brackets/Atom which are really sluggish in comparison). Admittedly, GUI software isn't always the first thing that people think of when talking about "system software", but i feel like that disclaimer is a must anyways, there's lots of nuance to everything out there.
But unless he includes situations where you must have zero dependencies (besides what is already expected to always exist, like a C compiler) among the situations where you have no other choice, then I disagree with him.
My last project and my current project are both in C. I hate that that's necessary, but the truth is that C is supported basically everywhere. It means that I can claim zero dependencies because it's de facto true, in that no user has to go and find other things to make my software work.
For example, if I wrote in Rust, the user would have to install Rust if they don't have it already, so I would consider that a dependency.
Of course, the fact that I am writing in C means that memory bugs can happen, so I had to have a plan for getting rid of them before release, and I do. (See [1] for an example.)
I guess what I am trying to say is that there's (unfortunately) still a place for C, and that (unfortunately) that place may be bigger than at first glance with the existence of things like Rust.
And I think that will remain true until Rust or some other competitor is as ubiquitous as C.
[1]: https://git.yzena.com/gavin/bc/src/branch/master/manuals/dev...
Yes. They can be a lot slower, because they might still need whatever it is that state brings to the table (f'r instance, caching, or argument suites).
They often need to rebuild the state, each time, or make it someone else's problem.
I run into this with CRUD stuff, all the time. I’m not an FP programmer (they do great stateless stuff), so I am sure I could do better.
These days, I spend as little time as possible, in the backend.
This made me laugh out loud. I’m genuinely curious what compels someone to write an article like this. People write C/C++ because they’re either paid to do so or it brings them joy. Just like literally every language that has ever existed or will ever exist.
> Yes, you can write things in C and C++ that are very fast, but statistically, it is unlikely that you have the skill and discipline to do so consistently and at the same time deliver quality and robustness.
I would love to know how to get to this level. C is such an elegant language, and I really enjoy programming in it. Seems like there must be a way to get really good at C... But how?
Now, unless you are coding for the Linux kernel or Postgres, it is a boat anchor. C++ will remain a sensible choice for at least two decades. Others are a crap shoot.
Please use whatever the fuck you like to write your software and leave people alone in their choice of tools.
Enough with this cargo cult belief system. Even if your choice of bicycle has training wheels, you can still fall down and scrape your knees if you don't know what you're doing.
I mean, Rust already has a few CVEs for use-after-free vulnerabilities, which this cargo cult swears are rendered impossible.
Just stick with your personal choice of worktool and own it. It's not the guardrails that stop you from crashing, but the way you drive.