System programming is quite poorly defined in general. I’d call it “the kind of software usually written by people who write C instead of Python”, and leave it at that.
> Just look at how badly Apple struggles with memory leaks now that they have switched CPU architecture and all the latent gremlins in their code start to manifest.
Ye I feel the author has no clue what he is talking about.
"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.
I did not mean it is 'simple' to write fast C code, but that it is 'simpler'.
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.
From what you wrote, it appears you don't know the meaning of "reasonably", "decent", "exception" and that you set your mind to disagreeing with the author before even fully comprehending the post.
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.
> You can write reasonably fast software in almost any decent language (with a few exceptions). But in order to do so, you have to have some idea of how to design and write software in mechanical sympathy with your surroundings.
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 tend to disagree. (Modern) C++ is an incredibly powerful programming language. Contrary to some other languages it gives the developer maximal freedom and does not impose a particular way of doing things on the developer.
In theory, yes. In practice few people use C++ fully, too often you find in-house "style-guides" vetoing specific things, such as Google's famous "no exceptions".
At the point you rule out using available facilities of the language you might as well use something else.
Practical, old languages tend to do this. In C++ or Common Lisp, which share little other than being multi-paradigm (unopinionated), it is fairly common to have house styles or accepted subsets.
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.
Both C++ and Common Lisp suffer a huge accumulation of historical baggage. It is this that makes the languages problematic, not being multi-paradigm. Common Lisp has first/rest as well as car/cdr. It has streams and numbers and the functions on them seem generic but aren’t (always) generic functions. It still has rplca despite (setf car) being a valid function name.
Sure; but "historical baggage" is a function of multi-paradigm and outliving multiple generations of computer architecture. I would not view baggage as necessarily problematic. What makes C++ problematic is that template metaprogramming evolved in a very ad hoc way, and now we need to backwards compatible all of it.
My claim is that these multi-paradigm languages may be better thought of as a core plus some paradigm-specific sublanguages, all jammed into the same syntax and glued together in random places. A better multi-paradigm language could avoid having separate parts that poorly work together. The thing that the comment I first replied to called ‘multi-paradigm’ is I think really the bad glue job like a plate that was smashed, glued together, smashed, and then glued together a second time. I don’t think it is an essential quality of multi-paradigm languages. For example Raku (fka perl6) fits different paradigms together more smoothly, and Julia can be used in a procedural or functional way as well as a more object-oriented way (though Julia’s objects tend not to contain as much state as a typical object-oriented language)
> In theory, yes. In practice few people use C++ fully, too often you find in-house "style-guides" vetoing specific things, such as Google's famous "no exceptions".
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.
Enjoy the source code of Android, Windows and macOS frameworks.
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.
You haven't seen safety relevant code then. High SIL and ASIL levels (combined with those systems being embedded) result in such rule sets for a good reason.
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.
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.
> 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.
Indeed the details matter, but my example was just designed to be something that might be somewhat more recognizable than those situations and restrictions I've encountered personally.
> (...) my example was just designed to be something that might be somewhat more recognizable (...)
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).
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=
> Basically, non-local control flow is dangerous (...)
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.
> Exceptions are a transparent and clean way to handle exceptional events.
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.
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:
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.
> Contrary to some other languages it gives the developer maximal freedom and does not impose a particular way of doing things on the developer
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.
Your assumption seems to be that if:
1) an existing language has finite utility
2) a language where any string is a valid program has no utility
, 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".
> Your assumption seems to be that if: 1) an existing language has finite utility 2) a language where any string is a valid program has no utility, that it must be true that utility decreases with the unconstrained-ness of a language (and thus increases with more constraint).
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 C++ is just fine. Memory management is not that hard anymore using smart pointers. I prefer that to java at any time of the night or day. Such an ugly verbose language. The buzz word laden stuff is actually quite bad. I have seen what could be rather simple systems be very unreliable and slow because of the many microservices all in their own container. Then if one is sensible enough to avoid that there is another pitfall. The cool kids like thread pools and having everything asynchronous. So a computation migrates between different threads many times between initiation and completion. I say instead: keep as much as possible single threaded and only offload expensive computations to thread(pool)s. That way one also does not need to lock the whole world, which, by the way, is far from free. Also, there are many situations where one can split computations in threads naturally as dictated by the problem one is trying to solve instead of dumping everything in a thread pool without considerations regarding what kind of queue system one implicitly creates. E.g., if one has N input channels and N is not too large one might have N threads for that.
This appears to be incorrect. I have seen a cppcon talk about that. I think it may have been this one: https://www.youtube.com/watch?v=rHIkrotSwcc . On the other hand, it is a very, very tiny minority of programmers who have to care about an overhead as low as this. Most of us should not waste one microsecond of our thinking time on this.
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.
Not when stepping through code in gdb/lldb in my experience in debug builds: they very often end up stepping in to the -> operator implementation for functions, which then requires me to step in again to get to the actual function call I wanted.
I'm not talking about code performance, I'm talking about human time of having to step through extra things while using a debugger to work out what's going on.
unique_ptr has zero overhead except when passing or returning by move. But I don't find myself doing that anywhere performance matters.
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.
This would seem to be a very micro optimization, though. If you are in a tight loop, sure, but otherwise I would say that smart pointers are a rather low price to pay. And if you are in a tight loop it may be better to try to avoid memory allocations altogether and try to reuse objects instead. In any case the price of smart pointers is orders of magnitude smaller than the price of what passes as 'modern' software development.
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.
> 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.
I've always found java much much more verbose. here's e.g. hello world in javafx:
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
> 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.
You know, to be honest with you, i feel like both are pretty verbose. Admittedly, with modern Java, you can slightly shorten the code there, it becomes the following:
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:
(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)
> 2. I basically don’t believe that memory management is not hard.
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.
I think it depends a lot on experience and wether the code base is post C++ 11. Modern codebases like clang or chrome do not seem to have any more memory management issues than something like Intellij. Personally I wrote system software in C++ for 10 years and memory management was a problem maybe once or twice.
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.
Never contain programs so few bugs, as when no debugging tools are available - Niklaus Wirth
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).
The other tools being talked about are static analysis tools or runtime instrumentation both designed specifically to try to find bad memory use patterns.
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’m merely trying to respond to a comment that claims that chrome should have few memory management bugs. I don’t really have an opinion about whether it is modern. I don’t care enough about the distinction between C++ styles to know where to find evidence of the rate of memory management problems in modern C++ codebases. Perhaps I would just find that the definition of modern C++ is ‘the style of C++ where you don’t write code with memory management bugs’ (I.e. if I were to try to find an example it would not be a true Scotsman)
I do not have single explicit memory management call in my product of very decent size written in modern C++. So no , I have no idea where did you fish your "evidence" from.
I don’t understand what you’re trying to say? Do you mean that you don’t call malloc? Because that is totally not the point. For example, you could create a new object, pass a reference to it to some function that stores it in a hash table, reach the end of your function where the object is automatically deleted, and then have something try to read the object out of the hashtable. Nowhere do you need to call malloc or do any explicit memory management and yet you have a use-after-free bug. Some evidence can be found in my reply to the sibling comment which talks about chrome, a large and very well-tested C++ application mostly in a modern style. There are also surveys of security vulnerabilities that find a large percentage are due to incorrect memory management.
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?
>"For example, you could create a new object, pass a reference to it to some function that stores it in a hash table, reach the end of your function where the object is automatically deleted, and then have something try to read the object out of the hashtable."
Emplace constructs copy of object in the container. You should be fine.
What are you trying to argue here? I’m not claiming that the thing I described is good, only that it is a way to get memory management wrong without calling malloc, so the fact that emplace exists is irrelevant. I think I still don’t understand what your point is because you seem to be arguing against something I’m not claiming.
In your example, storing a pointer to an item in the hashtable in a context that potentially outlives the hashtable items could qualify as "bad memory management". Long-lived cross-context pointers in general I frown upon (most pointers I use have their lifetime bound to a function call), although there are no hard rules of course, and you have a point in that these errors are not detected by most type systems or probably even static analyzers. Well, it's still a smell, because we know these types of problems and we know many ways to avoid most of them. I'm saying this all to support the claim that memory management needn't be hard, not to claim that there aren't any exploitable memory bugs out there, which would be an insane claim.
Also known as MFC, PowerPlant, Turbo Vision, CSet++, Taligent, OWL, ATL, Symbian, Motif++,... all on average 5 years old when Java came into the world.
Or Java was actually created by C++ developers that kept their habits.
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.
There are multiple types of system software, and it wasn't specified which.
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.
Quite often (at least in HPC) you equally care about correctness AND complexity/memory-usage and performance: there's no point being fast if the result's wrong, but equally, there's often no point being correct if the result takes ages to calculate, as the premise is calculating things in a particular time frame for the complexity budget of an algorithm.
Computing wrong results is rarely ok. But what matters is not only whether something is wrong but more often what are the consequences of being wrong. For example bugs in graphics rendering in games may be annoying but may cause long lasting effects such as data corruption, so it may be argued that whatever tooling you use to program your graphics can tolerate a bit more of sloppiness than the tooling you use to program your filesystem layer.
I assume a lot of people roll their own containers and use STL algorithms, which makes a lot of sense if you need control over allocations but still want to utilise the STL.
Yes. I think this is so common that Rust, positioning itself as an option for a systems programming language, has the #![no_std] macro to not link with the full standard library.
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.
Language preferences and safety features aside, I for one am 100% convinced that a/the upcoming language to rule "systems programming" (in a FOSS world with community participation) MUST have strong support for dynamic linking.
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.
How does having strong support for dynamic linking prevent someone from copypasting insecure code or just linking statically with your .so? Dynamic linking just makes deployment and compatibility a pain for some very dubious advantages
> How does having strong support for dynamic linking prevent someone from copypasting insecure code or just linking statically with your .so?
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).
I agree. But I also think that the following two language features are important:
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.
This is hilarious: someone comments about a real problem that requires a practical solution in a programming language, and someone invariably rushes to add their subset of “nice to have” language features.
Well motivated choices though. Polymorphism reduces lines of code, which are correlated with bug count. Reliability is essential for critical system software.
Specialization eliminates nearly all overhead of polymorphism so there's no disincentive against using it.
> It's got to be any package maintainer's worst nightmare.
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.
I guess one can work around the lack of dynamic linking in a language like Go on the package manager and/or distro level (though some established distros and package management ecosystems, which I very much hope will not go away, will have it harder to adapt than others), but I really would like to make everyone think about whether we should...
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.
> I guess one can work around the lack of dynamic linking in a language like Go on the package manager and/or distro level (though some established distros and package management ecosystems, which I very much hope will not go away, will have it harder to adapt than others)
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.
It is not uncommon that older/senior developers hold an undeserved grudge against STL. STL was far from mature in the 90s, sometimes even rather bad (at least in Windows/Visual Studio). But that is simply not the case anymore. Modern STL is well-written and highly optimized IMHO.
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.
From my point of view the STL still suffers from a major flaw versus the compiler frameworks from the 1990's.
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.
The allocation patterns that the STL requires / encourages, as well as the usability side (error messages) and the compile speeds, probably cannot improve unboundedly, given that they API has to stay the same.
IME the main problem of the C++ stdlib isn't the implementation quality, but the interface design. It must be everything to everybody, but at the same time doesn't provide much control over the internal behaviour. And the interfaces can't be changed because of source code and binary compatibility requirements.
In many (most?) cases, writing your own stdlib alternatives still makes a lot of sense.
Is it me or is this essay really an incoherent mess? As much as I would like to agree with the author's stern verdict (and I am saying this as a systems programmer with more than 20 years experience) I cannot make out the central argument.
The central argument is that C/C++ is not worth the trouble for new code and that using it "correctly" is a theoretical thing that even experts struggle with. C/C++ programmers keep on insisting that they've found ways to do it right, finally. Some C/C++ programmers seem to believe that, unlike everybody else, they actually have the combination of wisdom, skills, and discipline to not fall into this trap. At this point that seems rather foolish.
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.
> unlike everybody else, they actually have the combination of wisdom, skills, and discipline to not fall into this trap
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.
This isn't a "know more" or "know less" than me thing. This is being-a-human thing. I've never written a large amount of C++ without a similar mistake creeping in. Humans make mistakes.
I have to say, when I see new articles where the main argument is that when the author sees new projects being announced that are implemented in C or C++ they quickly lose interest, I quickly lose interest in the article.
The Rust propaganda is getting more subtle I see ;) It's only mentioned once towards the end of the post together with Go and Java. What a clever disguise.
The Rust propaganda or the anti-secure software propaganda that always starts to complain about Rust when one talks about writing safe software, regardless of which languages are suitable for such use cases since 1958?
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.
> 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.
>"Please don’t use C, C++ or Objective-C for system software where you have a choice. These languages are terrible choices when compared to more suitable languages."
Please use whatever the fuck you like to write your software and leave people alone in their choice of tools.
I really want to rid the world of memory bugs, so you think I would agree with TFA.
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.
I am a professional software developer for over 16 years, and learned C in the last 3 or so years for interests sake. I would most likely fall into the category of C developers that think they know enough tricks to get by, but probably write terrible code without realizing it.
> 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?
It is particularly foolish to pick up C at this late date. There were reasons 40, even 30 years ago.
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.
> Apple can’t do it. You probably can’t do it much better.
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.
157 comments
[ 3.2 ms ] story [ 247 ms ] threadWhat the hell is this referring to?
"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.
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.
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).
At the point you rule out using available facilities of the language you might as well use something else.
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.
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 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.
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.
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.
https://en.cppreference.com/w/cpp/compiler_support/20
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).
> 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:
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 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 You can tell for sure that the result has some error that needs to be handled. Your original code: This code no longer compiles in an absl::Status world. Instead you'd need to do something like: 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:
This could be handled by destructors in this case but in other cases: 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: 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.
That's not the case, the no exceptions rule applies to legacy and non-legacy code: https://google.github.io/styleguide/cppguide.html#Exceptions.
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.
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.
I think it is.
, 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 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.
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:
here's a Qt version: 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 C++ would just be or maybe 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:
> 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,
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:
(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.
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.
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.
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.
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.
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
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.
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.
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.
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.
In many (most?) cases, writing your own stdlib alternatives still makes a lot of sense.
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.
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-...
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.
> 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!
Please use whatever the fuck you like to write your software and leave people alone in their choice of tools.
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, 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.
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.