Safety: A comparaison between Rust, C++ and Go
nested.substack.com
nested.substack.com
`clang-tidy` is not letting you mutate or even access that moved-from "suffix" object without throwing an error.
It's annoying that you need `clang-tidy` and ASAN and shit to get comparable runtime safety even in greenfield C++, but that's not why to prefer Rust.
Prefer Rust because of a better traits system and pattern matching and syntax for `Maybe/Either` and a consistent build/library story and a number of other things.
But Rust vs. C++ is not an apples-to-apples comparison in this regard: Rust got a clean slate and was willing to cut ties with engineer-millenia of existing high-value software to do it.
In a perfect world Rust wouldn't be ambivalent at best and hostile at worst to C++ interop, c'est la vie.
Edit: how many languages have native C++ interop that supports the whole language? Would love to hear of any.
Slap it in an `unsafe` block, fine. But let me move a `std::vector<std::string>` into my `unsafe` block easily. Erase the types, fine. But let me call `v.at(idx)`.
Python takes C++ interop seriously, which is why Tensorflow and PyTorch and all the other people trying to script gigantic, extreme-value C++ codebases use it. Try `pybind11` sometime, it's night and day.
Edit to reply to edit: `pybind11` supports an absurd amount of C++ out of the box with completely natural semantics and a very modest performance penalty. So, Python.
Top ten best open source language efforts on Earth. Maybe top five. Just legendary stuff.
I do think that for all its faults though (cough packaging cough), the Python community displays an incredible commitment to getting shit done and helping people solve their problems. It's really a rather mediocre language as these scripty things go, but it just friggin works, which is why everyone uses it for everything.
Of course it relies on the module/extension API from python that is quite amenable to this kind of full-object and their methods binding. But it works very well together.
So much that we asked similar for Ada https://github.com/AdaCore/Ada-py-bind (Raphael Amiard from AdaCore is quite the Ada hacker) which, even with the limitations of Ada generics, has been a godsend for scripting Ada objects from python.
Is it something that a true polyglot hacker needs to know? It's got important, novel ideas or unique applications that aren't addressed well by other tools? I love learning languages, but there are so many you'll never learn them all well.
I'd say the driving principle is the language tries to have you write code that can be read easier, and is less concerned about helping you write millions lines of code per month. One example (when you get past the knee-jerk 'whoa it uses begin and end instead of braces') is generics where instantiation is mostly explicit (and thus can be painful to write) which helps reading, a lot. I'd downvote to hell any change to that (let the IDE/langserv generate the code for you, but please, generics are complex already).
The other driving principle is finding defects as soon as possible. So the compiler is harsh, there are lots of static checks. But it also helps you find defects at runtime as soon as possible. It can insert validity checks everywhere, if you can pay the runtime cost (hint: it's very rare you can't).
It has strong typing, very interesting typing features, no implicit conversion, most features you'd want in a modern language (tasking, OOP, generics, exceptions - erm...) and some that are more 'original' (contracts, quantifiers, if- and case-expressions, function arguments qualifiers in/out/in-out) and very nice union types (discriminated records).
Recently it has been extended to formal proof, with the SPARK language, which is Ada with some language features disabled and an automated proof environment (based on Why3 and SMT solvers), which is also its own interesting new tech, with interesting advances these last years (proof of floating point operations, memory ownership,...).
I think I could go on and on. But if you can spare one or two hours I'd just go to https://learn.adacore.com/courses/intro-to-ada/index.html and run the tutorial (all in the browser) and read a bit about all the features, and just ask questions downthread, I'll try to monitor it (like I don't already monitor obsessively all my HN comments...).
I always end up saying fuck it and `extern "C"`-ing everything. It would be completely possible to make these tools work well, but the Rust ethos is "rewrite everything, pure Rust", at least in large parts of the community, and so these projects kind of never get totally dialed in.
Did you report those crashes? There's also crubit these days, mostly combining the autocxx and (c)bindgen approaches. (It's a very new effort, which is why it's being kept separate from these more established ones.)
I know the talking points (it's impossible to go on the Internet and not know all the Rust talking points) but you have a few beers with some Rust people and pretty quickly you're hearing how anything that isn't Rust all the way down is somehow like, tainted, with the dreaded binary quality of being "unsafe" as opposed to "safe". A serious Rust hacker who shall remain nameless once broke out that old chestnut on me: "A barrel of wine and a spoonful of sewage makes a barrel of sewage." in reference to Rust software that links to C. It's not a coincidence that somewhere, every conceivable library that anyone could ever want is being rewritten in "pure Rust".
And it's a shame because Rust is fucking cool and I want to be using it even more than I already am. But I'm not going to become a Scientologist to get into a party in LA and I'm not going to throw away a mountain of excellent C and C++ because it's, unclean.
I'm optimistic that as Rust continues to have its center of gravity migrate away from strictly OSS and into ever higher stakes industrial settings (which it's clearly making great headway on) that the religious fervor will mellow and it'll become a "getting shit done" language that also happens to be a really cool language!
Working with any of the following is a nightmare:
- Memory arenas
- Intrusive data structures
- Buffer lifetimes with interesting lifetimes (typically because it maps to hardware)
At least until Carbon is ready! But that's a few years away at the least.
Confused... so you're suggesting Rust's checks are somehow free of false positives? Doesn't the halting problem get in the way? One Rust-specific example: https://www.reddit.com/r/rust/comments/nr7a33/is_the_borrow_...
I don't agree at all with your comment, and I find that sort of opinion miopic and not grounded on real-world software projects.
I 've worked on a fair share of legacy projects which were ported to the latest and greatest projects, including a couple of nightmare JavaScript ones. The very first thing we did was onboard onto static code analysis tools and source code formatters.
Once we enabled them we were faced with a big wall of red text dumps. With time that wall shrinked until there was no more red, and from thereon things stuck that way.
There was no trauma, only a kaizen approach to errors being flagged.
The whole C++ world already does this for decades, whether it's for compiler warnings, static code analysis, memory checkers, fuzzers, etc. What exactly leads you to believe this is traumatic?
Clearly the positives can be just as false in Rust as in C++. Your actual objection is that anyone arguing that any feature of Clang can measure up to the corresponding feature of Rust at present is automatically disqualified from making that argument because... Clang's past "taints" its present? Like an original sin of sorts, but in programming? ("Apple" forbidden against comparison?)
Meanwhile, Rust has always had those checks, so there can't be any Rust code in Production that doesn't pass them that would be painful to switch over.
Memory safety in serious systems software is something that you approach asymptotically and/or probabilistically. Rust makes it easier to be memory safe in a lot of scenarios, at the cost of the father-knows-best borrow checker, but a crashed program is a crashed program whether I dereferenced a null pointer or was poking around in a slice with multi-byte Unicode characters in it. And that's before you get to `rg unsafe` on your favorite industrial-strength Rust codebase.
Rust is cool for so many great reasons that get talked about so little because everyone seems too busy acting superior about memory safety. Talk about traits! Or Cargo! Or the cool async stuff! Anything but another lecture on memory safety.
They aren't the same thing though, that's the point.
Dereferencing a NULL pointer isn't guaranteed to crash. In fact if you are writing through the pointer, you may even have a security issue on your hands (rce, etc).
Safe Rust may have runtime errors that "crash" the program but this is a controlled, well-defined termination, and there is no way for the execution state itself to be corrupted like in C++.
I've spend weeks chasing down single instances of this on multiple projects. The nasty part is you have no predictability in if it's going to be one that crashes immediately, silently writes garbage(hopefully not to disk!), is a latent lurking crash or security vuln.
If you trash the stack then there's a good chance you lose the backtrace as well which can make a hard to debug issue become "find the needle in the haystack". I hope it's something that reproduces quickly and consistently because otherwise you're in for a ride.
These days you use `std::unique_ptr`, build with clang-tidy, CI under ASAN, and it's never an issue in practice. Once in a blue moon the CI chirps an ASAN failure that gives you the entire history of the memory address with line numbers and you fix the typo.
The safety that Rust gives me is that it's more expressive type system and modern affordances for things like exhaustive pattern matching lets me avoid logic errors, which are every bit as deadly as buffer overruns and much harder to mechanically identify.
It is usually easier to write correct code in Rust than in C++ because it's much more modern and frankly kind of an everyman's Haskell (which I mean as a compliment). But it's intellectually dishonest to say that this doesn't come at a cost: when you wander out of the borrow checker's sweet spot it can become kind of a Tetris puzzle even when you know all the rules on paper.
The same pattern matching that lets people see a borrow checker puzzle and immediately say "right, we need to do X" is the pattern matching that let's a C++ hacker see a failed template instantiation and immediately know what got misspelled.
In my experience there also tends to be a long tail of memory corruption bugs. After flushing out those that are easy to run into or that have a major impact, everything seems fine and you can go years without really spending time on them, but they're still lurking at the edges of automated crash reports and mysterious bug reports you never quite manage to reproduce yourself. And when I do manage to track one down, it's as likely as not to be in, around, or even caused by modern C++ features.
Tetris puzzle or not, it's really quite nice to systematically rule out those kinds of issues. In some domains it may not be worth it, but in others they can hide major security issues or similar. And either way it sure beats periodically digging through crash dumps trying to piece together something that looks impossible from the surrounding source code.
If your code is covered, ASAN will red-zone the memory bug. It checks every address.
People are welcome to their subjective opinions about the “easiest” way to get truly correct software (which almost no one needs), but the oft-repeated assertion/implication that the tools don’t exist to do it outside of Rust/Go is wrong. Not a subjective opinion, demonstrably incorrect.
And when enough truly important shit is written in Rust, which will be soon, there will be CVEs. Many of them.
(There are also cases where that extra robustness is more of a "nice to have," so if you can get a side effect of your approach to something more important, that changes the calculus too.)
One is categorically worse than the other though.
If it really never happens where you work, consider yourself lucky.
I completely understand that when you turn all the checks up to maximum (which, in fairness, `rustc` does by default) you start with as many errors as you have files if you're lucky, and probably 10x that. I've had to take codebases from working by accident on every 10th line to passing all the static analysis cheaper than PVS-Studio, and it's a bear no doubt. But codebases that are `clang -Werror` clean, `clang-tidy` and `cppcheck` clean, ASAN/MSAN/UBSAN clean, and have all this enforced by CI?
I haven't seen those codebases thrash the core dump where GDB prints out a bunch of "????????????" instead of addresses with any frequency.
Someone should do a 2022-edition "Joel Test" (https://www.joelonsoftware.com/2000/08/09/the-joel-test-12-s...) because I think we're all using revision control now, times change, but until someone does, I'm happy to trade war stories about getting messy code bases / development workflows into fighting form.
There are firms that will sell you a suite of frontends supporting every C compiler back to the early 80s and integrate them into a modern toolchain, I used to work with an alum of such a firm and I gather it's great stuff. I also gather it costs whatever you can afford, so there's that. I forget the name of the company but I could ping my friend if that's interesting to you.
Worst case, you could not have to chase memory corruptions on the subset of your target platforms that LLVM targets.
I recommend compiling with warnings turned on, and acting on them.
This just doesn't capture the problem that memory safety solves. A crashed program is not the worst-case scenario that it's trying to avoid. Even the most memory-safe language supports exiting early with an error message, or whatever.
In terms of language semantics, there is an all-or-nothing line between memory safety and undefined behavior. A memory safe program does what it says, locally, step-by-step, according to the semantics of the language. When a program exhibits UB, those guarantees are lost.
Of course, as you note, unsafe Rust also lets you violate memory safety, and in fact any memory safe language is at the mercy of its implementation and host. The reason people get evangelical about Rust's memory safety is one level higher: it offers a bridge back to memory safety, such that unsafe code stands on the same footing as the core language. When either are bug-free, the compiler can ensure they are used correctly, using the same type system features for both.
Modern C++ is certainly much less error-prone than the bad old days of manual `new` and `delete`, but it doesn't have an answer to this "unsafe encapsulation." To the contrary, modern C++ actually adds a bunch of new ways to violate memory safety by misusing library APIs. Iterator invalidation, use-after-move, string_view and span and borrowed ranges, by-reference lambda and coroutine captures, etc.
This all means that "serious systems software" in C++ has to approach memory safety via defensive copying or refcounting, copious use of sanitizers, and sandboxed sub-processes. Meanwhile, Rust programs can do things that would be unthinkable in a large C++ codebase, because the assumptions of both the language and unsafe code are encoded in the type system. (For example: https://manishearth.github.io/blog/2015/05/03/where-rust-rea...) It's a qualitatively different solution to the problem.
> Many years later we asked our customers whether they wished us to provide an option to switch off these checks in the interests of efficiency on production runs. Unanimously, they urged us not to--they already knew how frequently subscript errors occur on production runs where failure to detect them could be disastrous. I note with fear and horror that even in 1980, language designers and users have not learned this lesson. In any respectable branch of engineering, failure to observe such elementary precautions would have long been against the law.
-- C.A.R Hoare on his Turing award speech in 1981.
And I think that should be the killer feature of a language: that cool software is written in it and is continuing to be written in it. This is a killer feature shared by Rust and C++ and these days to be serious about performant software in diverse settings, you pretty much have to know both well.
I'm not going to go through every one and remember/look up which ones are written in Rust, but I'll wager that half-ish of them are.
I immediately thought: well what about Go for command line tools? Is this the viddy you speak of? https://github.com/sachaos/viddy If so, looks like it is written in Go. Looks like fzf too.
I'd really like to see that. Would be cool to see a completely new Linux user space written in Rust. Not necessarily a rewrite of existing software, new ideas would be great. I tested Linux system calls and they worked very well even though they needed experimental inline assembly functionality to work. With system call support, anything is possible.
Unfortunately it died a couple of years later.
It's taking a while.
Most of the biggest advances in software engineering are because of increased modularity. One of the best traditional ways to increase modularity is the ability to define and call functions. But any isolation between these "function" modules is only possible if you can at least factor out things into a function mechanically without introducing crashes (for example because of memory unsafety--modularity would fly out of the window right there).
>Rust is cool for so many great reasons that get talked about so little because everyone seems too busy acting superior about memory safety. Talk about traits! Or Cargo! Or the cool async stuff! Anything but another lecture on memory safety.
It's better not to dilute the message. All these other things are nice-to-have gimmicks. But the memory safety is a game-changer. It does no good to advertise 230 features at the same time. No one will remember. Advertise the killer feature. And that's the lifetime stuff, which gives you memory AND THREAD safety.
Rust's type system can do very little to help when those threads are accessing the same record on a database without transactions, OS IPC on shared memory, manipulating files without locks, handling hardware signals, handling duplicate RPC calls,...
Yeah but that ultimately requires an unsafe block, kind of true, except no one reads the code of all crates they depend on, and the direct dependencies might be safe in what concerns the direct consumers.
One thing that Rust’s type system does allow you to do is define a consistent manner in which to access external systems, even add types that will mimic the same safety. Is it perfect? Will it protect you from a different process working against the DB? Will it enforce things in the other process? No. But will it give you higher level semantics to be able to construct a better model for operating against that external system? Yes.
Maybe many Rust devs don't care.
Even if we account for everything, solar storms will eventually flip bits unexpectedly. Does that make Rust’s guarantees worthless?
Data race isn't just "Oh it's just a race condition" or Rust wouldn't care, data races destroy Sequential Consistency. And humans need sequential consistency to reason about the behaviour of non-trivial programs. So, without this writing concurrent software is a nightmare. Hence, "fearless concurrency".
You won't destroy sequential consistency by having non-transactional SQL queries. Try it.
It is obvious it is a subject that is irrelevant in the Rust community.
Who needs consistency in distributed systems when multiple threads from the same process are accessing the same external data without coordination.
Programmers do. Programmers are human and so can't reason about the behaviour of non-trivial programs without sequential consistency.
If I was trying to debug software which sometimes mistakenly issues people duplicate tickets, I think I'd want to be able to reason about how the software works, and that's not going to be possible if it doesn't even exhibit sequential consistency.
I rather like Lamport's last observation about what happens if you don't have sequential consistency and instead your programs just put up with whatever was cheap/ efficient to implement (as will happen by default on a modern multi-core CPU): "verifying their correctness becomes a monumental task"
It was later proved that it's not merely "monumental" this is actually an undecidable problem in general which explains why humans aren't good at it.
So, this is important in principle to get right, and (safe) Rust does so. You are of course welcome to decide you don't care, why aim to write correct programs anyway? And for now at least it seems in our industry many people agree.
Considering the number of deadlock issues encountered by folks using async Rust, I think "fearless concurrency" is misinformation by Rust evangelists.
Deadlocks in production Rust microservice:
https://blog.polybdenum.com/2022/07/24/fixing-the-next-thous...
"Fearless concurrency" isn't "It's impossible to make mistakes" it's only "The mistakes are no scarier than they were in your conventional non-concurrent program".
Why do people say things like: "It's better not to dilute the message"? Better for who? That's sales/marketing language, not engineering language.
"The message"? Pardon my Francais, but WTF?
>Better for who?
Better for everyone.
When talking about a new thing, it would be really silly to emphasize how nice the logo is, how nice the package it comes in is, look at the awesome tape the box is closed with etc. If I turn the product off it even turns off! Look at the nice rounded corners of the device!
It even can do async! Just like Javascript and .NET.
Who cares!
What is the main strength of the tool, the pain point it was made to eliminate? Lead with that.
> That's sales/marketing language, not engineering language.
Leading with the actual technical novelty that actually advances the state of the art in production compilers is marketing? Well, I guess it's good marketing in a way.
The user will find cargo on their own in 5 minutes.
But N=1 for you: as a serious polyglot user of Rust who knows it well and uses it all the time: this shit is a huge turnoff. It's a programming language. On a long enough timeline all the motivated hackers will end up knowing many programming languages well, they all have pros and cons.
Trying to boil important engineering decisions down to a tweet so that we can stay "on message" comes off like something someone would do if they were selling books or training or consulting services attached to a technology, which a priori gives them an agenda other than giving good advice.
So to keep it short: help people pick the right tool for the job without an agenda.
Why, for example, would a Python developer pick up Rust? Probably because of the really strict typing addressing a major pain point for most Python developers and the trait system being somewhat analogous to Protocols, which any Python developer who has chafed with the dynamic typing is almost certainly already familiar with. With good library support for interfacing between the two, it's a more natural coupling than most people would think on the face of it.
That said, while I don't think a Python developer reaches for Rust because of memory safety, I do think it's still an important factor as it provides the guard rails that make it so someone who has primarily used a GC language and not had to concern themselves as much with managing memory can start using Rust knowing that the compiler is not going to let them accidentally shoot them in the foot when it comes to memory management.
I think you’re wrong about the language and community, though. It’s killer feature is it’s safety, be it memory, data race, or type. These are the reasons I was interested in learning the language. The fact that the tools make that easier is why I was able to struggle through the new concepts and actually be able to build useful things with it.
If the fact that people enjoy something as a general community turns you off, that’s not the community’s problem.
So the tradeoff is not relative safety against a little compile-time inconvenience. The tradeoff is against "no, you cannot code that thing at all, suck it".
So almost all discussion of relative safety (which Rust advocates would like us to think is absolute) carefully sidesteps the point that there is a very great deal that cannot be expressed in Rust at all -- and not because expressing those things would have been at all unsafe.
This sort of comment is far from fair and misses the whole point.
The thesis of this article is how programming languages compare with regards to safety.
It makes no sense at all to compare particular implementations and try to pass personal assertions on particular features of said implementations as broad assertions about the programming languages they support.
So a particular Rust implementation is bundled with a linter. That's pretty convenient.
It just so happens that the linter is provided by a compiler stack that is also one of the main reference implementations of C++, and said linter also supports C++.
Is it fair to thus claim Rust is somehow superior to C++ just because a particular implementation enables by default it's linter on Rust code but not C++?
> There's little reason for clang-tidy not to be part of the default clang invocation by now
It's perfectly fine that anyone forms their own personal opinion on what defaults a tool should ship with.
That says anything nothing about the programming languages and their safety though.
I try to keep in mind that on balance it’s a good thing that performant programs have become dramatically more accessible recently, and that most of the C/C++/Fortran/Haskell antagonism is a result of enthusiasm around that. For me it was BASIC -> C, but I imagine JS -> Rust is every bit as exhilarating.
But I do hope at least a few people read your comment and are inspired to learn a little background. Rust is cool because it remixed the broadly-accessible FP algebra, a kickass C++ toolchain decades in the making, and a big bet on linear typing.
I didn’t set out to be the jaded, un-hip old guy but here we are :) It’s a nifty new LLVM front end with type classes and the Either monad. Neat. Get off my lawn ;)
Devs that don't care only get to build on their own computers.
It isn't fullproof, but definitely helps to tame some cowboys.
* Rust has traits, but does not support OOP. Architectures where OOP is particularly effective are proving to be a significant challenge for Rust - GUIs are the obvious one, but also game development Rust projects have to invent new approaches.
* Option/Result make the code flow obvious, but having to return them in nearly all function calls is tedious. Rust has had several attempts at alleviating this problem, but still hasn't matched the convenience of exceptions.
* match is IMO syntactic sugar and its benefits for correctness are being oversold. The fact that one is basically obliged to use it leads to code that's sometimes too deep. Exceptions would cut through this error-handling noise, if they were available.
* The build story is convenient, but this had the unintended effect of encouraging dependency explosion. Adding typical crates results in dozens of transitive dependencies being included in a project.
The notable improvement that Rust brings to the table is machine-verified memory safety with C++-like performance, but that of course comes at the cost of having to adapt code to what the borrow checker understands. If one needs the feature, then the cost is worth paying, if not, Rust is more of a personal choice than an inevitable conclusion.
And finally, perhaps Rust's biggest sin is that it's big, it's complex and there's no end in sight to the complexity spiral, just like for C++. I can only imagine the chagrin of the Rust community as they see themselves competing with Go for many projects where performance is not absolutely critical and often being second choice exactly because of this complexity.
Among common uses for these more powerful features is to make misuse of the library into a compile-time error. Coding the library in Rust, if possible at all, would mean failing to prevent these usage errors.
The point here is that C++ puts more power in the hands of a library writer, and both the responsibility and capability to enforce safety in uses of the library. Rust jealously reserves maintaining safety to the compiler alone.
// hey don't pass in a temporary
auto make_appender(std::vector<int>&& suffix) = delete;
(which turns the provided code into a compiler error under both g++ and clang++) is adequate to prevent the immediate class of issues (namely, C++ allows const Foo& and Foo&& to bind to temporaries).Meanwhile, it's really really easy to make std::span dangle:
#include <cassert>
#include <span>
#include <utility>
#include <vector>
std::vector<int> append(std::vector<int>&& items, std::span<int> suffix) {
items.insert(items.end(), suffix.begin(), suffix.end());
return items;
}
auto make_appender(std::span<int> suffix) {
return [=](std::vector<int>&& items) {
return append(std::move(items), suffix);
};
}
auto make_appender34() {
std::vector<int> vec = {3, 4};
return make_appender(vec);
}
int main() {
auto append34 = make_appender34();
assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2})));
}The point we should take away is that is actually hard to invent plausible examples of the failure that we are being told Rust would prevent.
I don't agree with that. If you can guarantee that the data pointed to by the span will outlive your appender, then it's safe. And if you don't actually want to transfer ownership or incur the overhead of a copy, and you don't care if your input is a vector or an array, then it's the correct abstraction.
Replace std::span with std::weak_ptr (or a raw pointer), and replace the closure with a class (e.g. a tree where each node has a weak pointer to its parent), and tell me again that nobody would ever write that code. It's fundamentally the same concept: if your ownership model isn't ironclad, or if any of your assumptions are ever violated, then you can run into use-after-free.
Closures only allow variables that are effectively final, aka you can’t reassign them (the compiler will stop you).
But you could pass an object and update its internal state.
What I mean is that what Java really "should" have is const like C++. A C++ function with a prototype of:
int doSomething(std::vector<int> const &x)
tells me much more than the equivalent Java: int doSomething(final List<Integer> x)
Also C++ member functions can declare themselves as not modifying their "this" instance. E.g. there's no way to write this code in Java: int X::doSomething(std::vector<int> const &x) const {
...
}
which is an extremely powerful, compile-time checkable description of what we are doing.It's not that final is useless (probably wrong choice of words there), what it does is okay and it's correct to use it as much as possible, but it's a far cry from the static guarantees afforded by const-correct code.
I also agree that the correct approach is immutability by default, but that ship has sailed, and it's also an orthogonal concern to what I'm saying here.
template<typename S>
auto make_appender(S&& suffix)
{
return [perf_fwd_suffix = std::tuple{std::forward<S>(suffix)}](std::vector<int>&& items)
{
return append(std::move(items), std::get<0>(perf_fwd_suffix));
};
}
see:
https://godbolt.org/z/M9P4MK4a8Another small thing: the C++ in this article looks weird to C++ programmers because it qualified vector with std, but does not qualify move.
I don't have anything against taking pride in one's skill and craftsmanship, but excusing a tool's failings purely on the basis that one needs more skill to wield it and avoid those failings? I want to have that same level of skill and have my tool multiply my skill's output to the max, not have my skill wasted coaxing my tool to perform correctly.
If the implication is that the tool requiring more skill gives commensurate benefits, fair enough, but not if it's just hand-waving away obvious downsides.
It would be one thing if this required skill, but this example is downright silly. Some other people here have posted more reasonable version, that might actually occur in the real world(like the example with std::span)
It’s a difference between “look this is Wally” (duh, of course) and a game of “Where’s Wally?”.
Whenever a CVE is discussed people say that the error is obvious, and no good programmer would write like that. It’s easy in hindsight.
The bad C++ code is in the very first line of the "make_appender" definition: capturing the closure's environment by reference is nonsense: It is equivalent to returning a reference to an argument. It is not, then, a closure at all.
Using a correctly-defined make_appender would not, then, produce undefined behavior when you use it, with or without "move".
What the author has done here was to take a too-obviously wrong operation, returning a reference to an argument, but dress it up with syntax that will look less familiar to some readers, and pass it off as insightful.
But using a wrong function and getting wrong results is not surprising.
Piling on more uses after, that give more wrong results, does not reveal anything more.
When you need disingenuous arguments to make your point, it tells us more about your point than about the thing you are trying to make a point about. And, publishing anyway tells us more about you.
Returning that fake closure should evoke a compiler warning, if you turn on warnings.
If it is so bad, it should (in the sense of how things would be in an ideal world) not compile.
>It is not, then, a closure at all.
It is a closure because all variables are closed-over and there are no free variables in the lambda body anymore. That is the definition of closure.
>But using a wrong function and getting wrong results is not surprising.
In an ideal world, there should be a compilation error. (There is in Rust)
The majority of what's wrong in C++ is that it lets you do nonsensical (even dangerous) things, most of the time without even a warning (and not because it's technically impossible to warn--it just didn't occur to them). It's okay to acknowledge that--it's a product of its time.
>Returning that fake closure should evoke a compiler warning, if you turn on warnings.
That "should" tells me all I need to know. In the end either safety is important, or it isn't. Choose accordingly.
Yes, C++ can be a bad solution to a lot of problems, and that's okay. Use rust if you need a machine guarantee for memory safety (or you just like the language), you can use Go if you just don't care about that at all and want the language to take care of it. But you can use C++ for non-critical software that needs to be fast (games come to mind). Rust can be too much of a mental overhead than it's worth for some.
That closure is just not a good example. Nobody would write this, because when writing C++ code you _do_ think about whether you want a reference or not. Sure, a lot of bugs can happen but this is not, in my opinion, one of them.
If you want to prove a point, prove it fairly.
Note: I don't use C++ anymore, and I don't like it very much for other reasons.
Rust has the least mental overhead of any language I've ever used. The compiler literally takes all the mental overhead away.
Increasing the strictness of the language has one effect: decreasing the number of potential solutions for a given problem.
If the restrictions are carefully chosen (like they are in rust) this leads to generally safer code. But don't fool yourself, the restrictions don't merely generate new solutions - they reject the ones that don't pass the tests.
A more extreme example is formal theorem provers. Carefully constructed proofs will take a lot of effort, but it will also make you confident that the code does what it needs to do.
The rust borrow checker is just a more restricted theorem prover that doesn't touch the logic, it just deals with the memory side of things. It's indeed very helpful in trying to explain what's wrong (and even suggest fixes), but it doesn't take the programmer overhead away.
In a more complex system rust inevitably makes it harder to come up with solutions. It will reject valid code just because it can't prove it's right, not because it's actually wrong. As a programmer, you're going to come up with such solutions, and while in time you get more used to writing code that rust likes, and rust too gets better at accepting correct solutions, you're going to have to fight the borrow checker sometimes.
I don't have a ton of experience with rust, but I encountered cases where equivalent C++ code would've worked just fine, but I had to change it because rust didn't like it.
Rust is an amazing language, but it definitely doesn't 'take all the mental overhead away'.
Not to disagree with your post but this comes across as very black-and-white.
There's a prioritized list of requirements in engineering. Use the right tool for the job.
Rust has: (see https://hackmd.io/@rust-ctcft/r1plN4You#/5 )
1. Safety
If you don't need that at the first slot, then the biggest strength of Rust doesn't apply to your problem (and you would waste time having to track lifetime parameters more than necessary to solve your problem).
The .clone() would have been [=] in C++, which would have safely quieted the warning.
To me, with the 20 years of experience I have in 8 programming languages, I'll always include safety. Especially having in mind that it's nearly zero-cost in terms of runtime performance.
So to me not choosing safety is a strong sign that I don't want to work with the people who practice that.
So, safety, good, fast enough, good, but insufficiently expressive? No, thank you.
Want to elaborate on that? Quite curious.
#[derive(Debug, PartialEq, Eq)]
pub struct ColumnVector<F, const DIM: usize>([F; DIM]);
impl<F: Field, const DIM: usize> Add<Self> for ColumnVector<F, DIM> {
type Output = ColumnVector<F, DIM>;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.0.zip(rhs.0).map(|(a,b)| a + b))
}
}
Usage: let v = ColumnVector::new([1.0,
3.0,
4.0]);
let w = ColumnVector::new([1.0,
0.0,
4.0]);
assert_eq!(v + w, ColumnVector::new([2.0,
3.0,
8.0]));I propose you start with genuinely concerning problems. Operator overloading is a first-world problem and I've made a good career never depending on it (outside of my active C/C++ years at the start of it).
"Generics argument concepts" says exactly nothing, to me at least. Elaborate?
"Programmed move semantics" is, if I understand you correctly, a flavor / taste thing but I can agree it can be made better and more explicit -- say by not using the `=` operator for it. That I could stand behind. Still, it's only catching you off-guard while you're learning. 50/50 though. It's a concern but IMO not a major one.
And your final sentence betrays bias to C++. Well fine, use that, nobody is forcing you to work with Rust, right?
But if you're willing to bash Rust, please do so with concrete arguments. If you know something negative about it that I don't, I believe I and many others will benefit from informed objective criticism.
Do you have that? Already asked you in another comment and I'm still willing to listen to it.
std::vector<int> suffix{3, 4};
auto append34 = make_appender(suffix); // Version 1: test will work
auto append34 = make_appender({3, 4}); // Version 2: test will fail
I was in a mood to type the examples but I accidentally made Version 1 because I had started with the author's first example. I wondered where the problem was and couldn't find any (neither running nor reading the code) until I noticed the author had changed to Version 2.The problem is "obvious" in hindsight but one of the problems with C++ is that it makes certain things too implicit/convenient. I get that references are a staple feature in C++ but it's not at all obvious from the calling location that Version 2 is a bug. To see that, you have to navigate to the callee (finding which might be hard enough alone without a very solid IDE), and make sure that it's not capturing the argument by reference or not doing anything unsafe there.
It's often a problem when a seemingly "value" argument is turned into actually a pointer-to argument at the callee. There are other languages that have this too, and I've never liked it.
As someone who doesn't regularly code in C++ but has a solid understanding of the basics, I wonder why C++ ever allowed to have a reference parameter be called with a temporary? To me it feels like "References have value syntax but pointer semantics BUT you should program like it had value semantics"? Which to me would be exactly a premature optimization that is looking for trouble.
If you want copy in your C++ lambda, you start it `[=](...) {...}`, if you want take a pointer in your C++ lambda, you start it `[&](...) {...}`, if you want something trickier you do trickier stuff.
Rust opts you in to the nitpicky static analyzer and you have to opt out with `unsafe`, in C++ you have to opt in with e.g. `clang-tidy` or some annotations.
They are remarkably similar, just with different defaults.
I'm going to lead with this, because I think it's most important: Culturally there's a world of difference. Safety is a part of Rust's culture. "Culture eats strategy for breakfast".
Take sorting. In C++ the default sort is unstable, while in Rust the default sort is stable, that's just those defaults you mentioned (each has both kinds), although the choice speaks to culture. But look closer, in C++ the sort has undefined behaviour if your type isn't totally ordered. In Rust you can't sort the partially ordered types without saying how to order them fully. Still, in both languages we can write a custom order, so what happens then? In C++ if your custom order is nonsense you get... undefined behaviour. In Rust sorting won't necessarily work with a nonsense custom order but the behaviour remains well defined.
> Rust opts you in to the nitpicky static analyzer and you have to opt out with `unsafe`
Unsafe gives you a small number of dangerous "super powers" needed to write efficient low-level code, it does not opt out of the borrow checker's analysis, or indeed most other checks. This misconception makes me wonder how much of what you've written is conjecture rather than practical experience.
Well in Rust the default `HashMap` uses a cryptographic hash, and you see it everywhere, it's the de facto community "default". In C++ the community "default" is `absl::flat_hash_map`/`folly::F14`, which use SIMD to compare a whole stripe of key-prefixes simultaneously.
I want different defaults for different programs, but the idea that it's esoteric to ever want an associative container within arms reach that fucking demolishes the other one is, ugh, God I want to like Rust even more than I do but this "we're right and everyone else hasn't seen the light" routine is infuriating and pushes me at least away.
My parent comment is trying to emphasize that this isn't a right/wrong thing, different tools for different jobs. And people are just like: "nope, everything but Rust is wrong".
However, I’ve never gotten the sense that Rust itself promotes the idea that “everything except for Rust is wrong.” I also don’t read much on the Internet these days, and I’m not doing so, probably avoid much of the hype that people are pushing about Rust.
Since it has been established as Hot New Thing, there are huge social incentives tied up in promoting it.
Having the "right" defaults is better for everyone. Folks who don't know or care get a good, safe default with no undefined behavior or unexpected danger, and folks who know better can opt into something that fits their needs explicitly.
Seems ideal to me.
Whoops - that should have said "a language with better defaults"...
Just to be clear, the Rust HashMap does the same thing.
I don't know how I missed this, is the Swiss port a fairly recent development?
The port is a little older, 2018. The idea was famously explained at CppCon 2017, I don't know whether Google had published on Swiss Tables before that year.
So, the trade is just: slow for nothing.
Such algorithms are crucial to avoid being subject to a Denial of Service attack which is, in fact, a security problem. Of course under the C++ "blame the programmer" philosophy you don't deserve protection from Denial of Service unless you knew you needed that and figured out how to ask for it properly.
Just as with the sort functions this is about safe defaults, not about constraining people who know what they're doing. Dropping in FNV instead of SipHash, or even using the identity function as a "hash", is not difficult if you are sure that's what you need.
But to the degree that C++ has an RTFM vibe, and I really don't think you'll hear Andrei or Meyers or Sutter talking that way much, it's uniformly applied and not particularly partisan. In my experience C++ pros would rather be writing Haskell and that's where you get all these over-templated "header only" libraries.
Rust is in a glass house on "blame the programmer" stuff, because it's "blame the programmer for being dumb enough to not be using Rust exclusively".
The memory safety catechism makes sense for my SSH client, or web browser, or web server, or shell, or another few dozen security critical things. And frankly I'd feel safer if someone did a ground-up reimplementation of `bash` in Rust, I'd use it in a heartbeat. Tailscale writes their shit in Go for a reason, you don't want even the possibility of a use-after-free in your VPN.
But most of the software I run? It runs as me, and if someone is running as me behind the firewall, I'm in deep shit already.
`rustup` has `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` on the fucking home page, ditto vim/emacs extensions, etc. I'm calling bullshit on the every damned thing needs to be DoS or timing-attack hardened. It's marketing, and my original point is that all the other great modern features of Rust make a much better list of talking points.
But that isn't the claim. Rust's defaults are safe. Remember Rust's one line description "A language empowering everyone to build reliable and efficient software".
This is like with the decision that Rust's sort() is a stable sort. I know what a stable sort is, and so do you, so if we care we may decide it's appropriate to use the unstable sort which could be faster. But programmers who don't know what a stable sort is aren't expected to learn about it before their sort does what they expected.
Same here, I know that SipHash is slower than Fowler–Noll–Vo, which in turn is slower than the identity function, and I know why it would or would not be OK to choose them, and presumably you do too. So if we care we may choose a different hasher for our HashMap. But programmers who don't know about hash algorithms aren't expected to go learn all this stuff before using HashMap.
I think maybe C++ isn't programming it's actually a live action "Um, actually" game where the stakes are your program arbitrarily misbehaves unless you correctly guessed all the things wrong with whatever code you just wrote despite the compiler insisting there's nothing wrong with it as written.
Could I do OK at that game? I'd like to think so. Do I want to play? No thanks.
It doesn't help that those of us that care about enabling the right defaults are a tiny minority as per C++ surveys.
https://blog.jetbrains.com/clion/2021/07/cpp-ecosystem-in-20...
I think the point the author tries to make is that, while C++ and Rust are probably "the same" for the most skillful and disciplined programmers (such as you), for average human, Rust just catches way more errors they make for them early. An extreme analogy would be trying to climb Everest all by yourself vs with a professional guide.
I suspect that the adoption of Rust as a system language is going slower because that's just the natural pace of embedded/systems development, not because of anything intrinsic to Rust. There are now 30k+ lines of Rust code [1] in the Linux kernel. C++ can't claim that.
I was also unaware that Rust has such significant penetration in the Linux kernel, and that's a place where I can see it really shining.
My first comment in this thread was something to the effect that Rust has tons of great stuff to offer, and that the memory safety argument is actually weaker than people think and probably not the only thing people should talk about.
The resulting gang-tackle is just one more data point that the community is still too small and evangelical for me to want to get involved past my proprietary Rust stuff.
I’m using it for a new project and honestly I’m using it more for the modern tooling and easy C interop than the safety features, but I’m a fan overall. Think it’s a really good language.
Can you expand on that? I'm currently researching something similar but lower level & lisp instead of Haskell. It would help to see some existing examples to figure out if it's worth it or not.
Basically we have a nice Haskell DSL for generating arbitrary C++, and we deal with lots of code you wouldn't want to write by hand (big nested switch statements and other kinds of state-machine logic, choices about loop unrolling, lots of template overloads, SIMD intrinsics that require immediate values, etc. etc.) so we write Haskell that generates C++ and feeds it to e.g. `clang`.
Some of this is directly in Haskell, and some of it is little compilers mostly done using Megaparsec. It's a really nice approach where it fits!
None of it is really all that magical btw, so you should take a whack at it yourself!
One thing that wouldn't have occurred to me that one of the Haskell geniuses I work with identified to use is the notion of a "free"/"operational" monad: https://apfelmus.nfshost.com/articles/operational-monad.html.
> None of it is really all that magical btw, so you should take a whack at it yourself!
Of course! It's always good to see how other people approach it though.
That's just as true of Rust if you use clone(), Rc<> and RefCell<>. You just have to familiarize with a few boilerplate patterns, and the best part is you're only trading off a modicum of performance while preserving safety. But Rust can work quite well as a language for exploratory programming.
Industry clearly prioritises speed of delivery above all else. Security, reliability and maintenance are future problems (that would be nice to have).
However, in order to gain the power to reason about our code and be able to prove the correctness of various properties (what a typechecker does), we have to program with more restrictive models. This has been argued many times before (e.g. Dijkstra's structured programming). Haskell and Rust are just two of many examples. My favourite is regular expressions, choose actual proper regexes and you have guaranteed O(n) execution, choose Perl/Python "Regex" and you have a potential security hole.
There was (and still isn’t) a competitor that can compete on the same level with Javascript in the browser.
<source>:21:35: warning: temporary whose address is used as value of local variable 'append34' will be destroyed at the end of the full-expression [-Wdangling]
auto append34 = make_appender({3, 4}); test.cpp:16:45: error: Using object that is a temporary. [danglingTemporaryLifetime]
assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2}))); // FAIL: UB
^
test.cpp:3:12: note: Return lambda.
return [&](std::vector<int>&& items) {
^
test.cpp:2:50: note: Passed to reference.
auto make_appender(std::vector<int> const& suffix) {
^
test.cpp:4:36: note: Lambda captures variable by reference here.
return append(move(items), suffix);
^
test.cpp:15:35: note: Passed to 'make_appender'.
auto append34 = make_appender({3, 4});
^
test.cpp:15:35: note: Temporary created here.
auto append34 = make_appender({3, 4});
^
test.cpp:16:45: note: Using object that is a temporary.
assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2}))); // FAIL: UB error: Syntax Error: AST broken, binary operator '!=' doesn't have two operands. [internalAstError]
explicit operator bool() const { return this->get() != pointer(); }
This is for a simple wrapper class that looks like this: template<class T>
class Foo : private Bar<T> {
public:
typedef value_type *pointer;
pointer get() const { return ...; }
explicit operator bool() const { return this->get() != pointer(); }
};
Another example: void foo(uintptr_t const (&input)[2]) {
if constexpr (sizeof(uintptr_t) == sizeof(int) && sizeof(long long) == 2 * sizeof(int)) {
long long value;
memcpy(&reinterpret_cast<uintptr_t *>(&value)[0], &input[0], sizeof(input[0]));
memcpy(&reinterpret_cast<uintptr_t *>(&value)[1], &input[1], sizeof(input[1]));
}
}
error: The address of local variable 'value' is accessed at non-zero index. [objectIndex]
memcpy(&reinterpret_cast<uintptr_t *>(&value)[1], &input[1], sizeof(input[1]));
^There isn't a right and wrong here: sometimes you want to opt in to the check for that, sometimes you want to opt out of it.
Sometimes I actually do want to fuck with addresses on the stack in weird, potentially architecture-dependent ways, it's rare but it happens.
I happen to think that Rust's linear/affine typing is by far the most usable low/zero-cost memory management model that anyone has demonstrated at scale and a real achievement in practical computer science, but it comes at a pretty serious cost in `Box`-this and `Arc`-that and `Rc`-other-thing and generally the borrow-checker being a PITA about some stuff we're used to doing.
Rust is very cool and I use it, but the "using C/C++ is fucking strangers without protection"-vibe got old years ago.
What matters is the lifetime, where the object lives is a rule of thumb for guessing lifetime that results from C++ trauma.
Getters that return references are code stink.
source: consultant, seen some truly heinous Go monoliths.
It's only my gut feeling, but does that match your experience ?
append34 := func() func([]int) []int {
If I was going to rank the readability I would say:1. Rust function bodies 2. Go code 3. Rust function signatures 4. C++ code
Which you could argue is me shifting the boundaries a bit, but sufficiently statically typed languages seem to develop two (or more) sublanguages. Global complexity definitely pushes Rust down peg.
https://go.dev/blog/declaration-syntax
Go's type syntax is unusual but supposedly much clearer when things get more involved.
Probably not, and to be fair anyone else's preference is equally valid. I think largely what individuals consider most readable depends less on what's being read and assessed now, and more on what route a developer has taken to reach this point in their career.
If I'm honest it's more about unconscious familiarity with idioms and constructs than it is an isolated unbiased opinion.
And I'd probably also change my comment slightly to say that by "Go code" I really did mean the actual code doing the work; I find the tests far less appealing.
Probableur a typeaux?