Hacker News new | ask | show | jobs
Safety: A comparaison between Rust, C++ and Go (nested.substack.com)
113 points by ephesee 1422 days ago
14 comments

Rust has a lot of great qualities that C++ lacks, but comparing `rustc` to `gcc` or `clang` on move-semantics checking is just kind of silly these days. `rustc` has `clang-tidy` built in.

`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.

Your point is a fair one, but defaults matter. There's little reason for clang-tidy not to be part of the default clang invocation by now, other than an aversion to producing new output for existing projects (that you could argue are already "broken"). Unless clang-tidy has false positives, in which case the comparison isn't apples to apples then.
Oh I completely agree that defaults matter, and I wouldn't advise anyone to go start some big C++ project unless they had a very good reason to.

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.

Rust isn't hostile about C++ interop, it's that between native interop (which requires dealing with templates and memory unsafety) and safety, Rust prioritized safety, while Carbon is trying the alternate approach. The behavior of C++ is simply hard to interface with while providing the assurances Rust gives you.

Edit: how many languages have native C++ interop that supports the whole language? Would love to hear of any.

I said ambivalent to hostile at worst, and I'm sorry, it is. I write FFI to C++ in Rust, Python, and Haskell practically every week, and Rust <-> C++ sucks.

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.

I wouldn't say python takes C++ interop seriously, it's more like the pybind11 people are amazing at what they do and found a way to slice the problem neatly. But yes, it's night and day, pybind11 is a godsend.
Thanks, I'll look at pybind11. Have you looked at cxx by any chance?
It's not just C++ interop. We chose Zig because writing C library bindings for Rust is bewildering.
Yeah.

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!

the first thing i did in rust was c lib bindings and it is simple.
Given the vast C++ ecosystem (including pretty much every big game engine), there are still a ton of very good reasons to start a new big C++ project.

At least until Carbon is ready! But that's a few years away at the least.

> Unless clang-tidy has false positives, in which case the comparison isn't apples to apples then.

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_...

There's a difference between code that used to compile no longer compiling because of an incorrect lint, and code that was never accepted. Rust is restrictive and gets less so over time. C and C++ need to become more restrictive over time, but that's a more traumatic direction.
> Rust is restrictive and gets less so over time. C and C++ need to become more restrictive over time, but that's a more traumatic direction.

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?

What you're actually arguing seems to be "why I like Rust more than C++", not arguing why "clang-tidy has false positives, and thus the comparison isn't apples to apples then".

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?)

I don't think it's about being tainted. It's that there's a bunch of C++ code out there already in Production in a ton of places that doesn't pass those checks, and may or may not actually be safe. It would be a ton of work that may not produce any end-user value for any C++ project to switch over to that.

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.

It's not about an original sin, it's about the practicalities of changing the default behaviour without pissing off your users.
I find it so weird that the Rust community is borderline evangelical about memory safety when a) it's not actually memory safe once you start doing heavy shit b) modern C++ is quite memory safe and c) there are so many other great reasons to like Rust.

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.

> 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

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++.

Yep, my favorite part of heap/stack corruption is not when it crashes immediately but rather when it rears its head 2-3 weeks/months later when some upstream call pattern or timing has changed.

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.

> a) it's not actually memory safe once you start doing heavy shit b) modern C++ is quite memory safe

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.

I think memory safety is the killer feature of rust, and has become so because people see the real world problem it's solving, more than through evangelicalism. We'll see in a few years when more "heavy shit" has been written/rewritten in rust. My prediction is that they will have significantly fewer memory safety issues than comparable c++ "heavy shit".
Business also needs to care,

> 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.

From where I sit the killer feature of Rust is that a bunch of amazingly cool software is written in it, especially in the terminal. I'm a big terminal guy, and I can't think off the top of my head of anything I use constantly that isn't written in Rust. `rg`, `fzf`, `zoxide`, `bat`, `viddy`, the list goes on and on, I fucking love the shit people are writing in Rust.

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.

> We'll see in a few years when more "heavy shit" has been written/rewritten in rust.

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.

>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

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 thread safety only applies to the special case of those threads accessing in process data segments.

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.

Rust isn't a startup business or (one hopes) a religion, or an MLM, or a home for sale. It's a useful tool among many.

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?

rustc rejects the resulting program if you mechanically factor out a function accessing &mut self, into a function holding mutable borrows to half the fields calling another function which access the other half of fields (or vice versa, the caller holding &field calling a method mutating other fields). This requires the more complex transformation of passing individual fields into the subfunction (more work, but sometimes easier to read), or waiting for Rust to add partial borrows. Note I've never actually hit this case myself, though I've heard it's an issue people run into.
Rust is "cool", lately. But it lacks basic features that enable capturing important semantics in libraries.

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.

i honestly don't think i've seen "cool async stuff" as a substring before.
> Your point is a fair one, but defaults matter.

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.

As is clear from my other comments I also find this stuff unfair.

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 ;)

They do, that is why any C++ shop where code quality is relevant has a DevOps team that cares about the right defaults being enforced on th CI/CD pipeline.

Devs that don't care only get to build on their own computers.

It isn't fullproof, but definitely helps to tame some cowboys.

To say the rust has a built in linter is wrong. A rust program that does not build because of a memory ownership error on the part of the programmer isn't rejected by the compiler due to a detected pattern, it is rejected because the program is "unsolvable" and cannot be built. I think a lot of people miss how integral the memory semantics Ruct enforces are to how it parses and compiles programs. If these things were simply lints it could be reasoned that a program could be built without following these semantics, that If you could simply go into the compilers source you could just turn them off/remove them. You can't. The way rust's compiler tracks memory is fundamental to how it compiles the binary. It is not simply pattern matching code or ast. Rust's compiler is actually tracking the lifecycle of every bit of memory allocated so it knows when to free it, and it does this at compile time without running the program. These memory semantics errors exist because they are integral. Turning them off would simply result in a broken compiler, or a program with no freeing of memory because the compile time reference counting rust implements becomes impossible.
Objectively speaking, Rust does not "have a lot of great qualities that C++ lacks". Any new feature or improvement has its pluses, but also its minuses.

* 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.

More to the point, Rust lacks key features I need to capture essential semantics into libraries. So, the libraries I could write in Rust would be less powerful than libraries I can write in C++.

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.

Could you elaborate on this?
And way, way better compiler errors thanks to a sane generics system
And much better compiler errors. `rustc` about leads the pack on error messages of all the languages I use regularly.
I think you meant thanks to a sane(r) macro system? Both Rust and C++ use monomorphisation for generics, I believe shitty compiler errors are due to C++'s templating.
I’m what way can you have ‘generics’ in C++ that are not based on templating? I am almost certain that any implementation of anything ‘generic’ templates are inherently involved. Maybe I’m wrong about what you mean by generics though.
There are concepts now which are close enough to Rust's traits.
I feel like a better comparison would be to use std::span (C++20) to mimic Rust's slice. Otherwise you might be tricked into thinking that adding

    // 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})));
    }
That would just be another tendentious example. Nobody would write a make_appender that takes a span argument, because it makes no sense.

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.

> Nobody would write a make_appender that takes a span argument, because it makes no sense.

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.

What about comparing to Ada Programming Language?
Java falls a bit in between Go and Rust on that example:

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.

"final" in Java is kind of useless, really. A const mechanism like C++ has would go a long way and would be a perfect fit for the OO nature of the language.
Final in Java means the value of a variable, property or parameter will not change after its initial assignment. Values in Java can be either a reference to an object or a primitive such as an integer, double or bool. It's definitely far from useless as it asserts that the reference or value you capture in a closure is not an old version that has been replaced, this approach is a consequence of Java disallowing arbitrary pointers. IMO Java's biggest mistake here is mutability by default, which Kotlin has learned from. If you understand why it is this way it makes a lot of sense and tbh I think it promotes better code. That said I would like to see more immutability in Java and with things like Record classes you can see Java is moving in the right direction.
I agree with that and I use final as much as possible. E.g. instance variables that don't need to change, I almost religiously declare them as "final" and initialize them in the constructor.

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.

The fact that the GC “owns” all of the memory happens to help a lot with lock-free stuff.
The author could compile c++ with the sanitizers, i.e. -fsanitize=address,undefined and make a make_appender function that leverages perfect forwarding...:

  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/M9P4MK4a8
There's a solid argument in here but it feels like there must be a better example. Can we think of a function that does something worth doing, in a way that programmers of all these languages would actually use, and that sets a subtle trap for C++ programmers? When I read this article all I see is a useless function that contains a completely obvious trap which, yes, Rust prevents, but also just thinking at all would have prevented.

Another small thing: the C++ in this article looks weird to C++ programmers because it qualified vector with std, but does not qualify move.

This would be a better article if the UB in the C++ example wasn't blindingly obvious-- no C++ programmer worth a damn would ever write this.
Why is this such a common defense for the peculiarities of C++? I see it pop up at least a couple of times, anytime C++ is criticized.

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.

Because the code in question isn't something a C++ dev would write, instead it looks like something someone who doesn't use the language much if at all, decided to use for this pretty silly comparison.

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)

Yeah, agreed that a competent C++ dev would not write the code in the example. The charitable interpretation though is that errors of the same type can crop in real codebases, the example is just simplified for the purposes of discussion.
In such examples you’re always primed to look for a mistake. It’s a different situation when you have 100K+ lines of code, a dozen developers, and a deadline.

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.

Yea and C++ static analysis tools already warn for the case, so even for new C++ programmers where it might not be entirely obvious, its still easy to catch the error.
More to the point, the compiler warnings would save inexperienced programmers too.
Can we throw Python in too? Python has more safety than any of these languages, yet it's always left out of these discussions.
Is this satire?
You're joking, right? A dynamically-typed language has more safety?...
What is Python lacking that would make it as safe as Rust?
Screenshot absolutely make me •¶Žš‰»‚¯
Disingenuous.

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.

>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.

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.

> If it is so bad, it should (in the sense of how things would be in an ideal world) not compile.

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 can be too much of a mental overhead than it's worth for some.

Rust has the least mental overhead of any language I've ever used. The compiler literally takes all the mental overhead away.

> 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'.

"Doctor, it hurts when I do that!"
Pain is a strong warning that prevents [further] trauma. This analogy defeats its own purpose.
The doctor says, "then don't do that". So, the analogy is correct, and you have missed the point.
I don’t think so. In this analogy you don’t go to the doctor at all, because you feel nothing until a bone cracks. The whole point is based on “but nobody would do that”, so in reality we should see no results of UB and segfaults outside of educational experiments. But the only way to do that is to keep our eyes closed.
>> In the end either safety is either important, or it isn't. Choose accordingly.

Not to disagree with your post but this comes across as very black-and-white.

To expand:

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).

There's also another item in that list, and that is performance. As soon as you slightly relax that one, Rust does become massively easier. A well placed .clone() or Arc makes the rest of the code performant enough and easier to understand, which makes the equation of choosing Rust over other alternatives in spaces that aren't necessarily systems programming less problematic.
Arc is equivalent to std::shared_ptr, and so ought to be code smell in Rust.

The .clone() would have been [=] in C++, which would have safely quieted the warning.

Big problem in these discussions is what should be prioritized.

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.

The point missed everywhere is that Rust lacks key language features needed to capture essential semantics in libraries, that C++ provides. To code libraries I want to code, I cannot use Rust. Rust cannot express them.

So, safety, good, fast enough, good, but insufficiently expressive? No, thank you.

    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.

Again, this isn't a right/wrong thing. Rust moves by default (and a lot of people find "=" a weird pun for that), C++ copies by default and has rvalue-references and an explicit `std::move`.

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.

> 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.

The sibling says that C++ has the wrong defaults, full stop.

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".

I like and use both C++ and Rust. I also have plenty of bones to pick with both languages.

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.

Why do you think that a language with better details would prevent you from opting in to whatever non-default behavior you need?

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.

> In C++ the community "default" is `absl::flat_hash_map`/`folly::F14`, which use SIMD to compare a whole stripe of key-prefixes simultaneously.

Just to be clear, the Rust HashMap does the same thing.

In fact the crypto hash does not actually improve security. It is just slow.

So, the trade is just: slow for nothing.

There lies the problem with C++ state of affairs, most defaults are wrong.

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...

You can also 'const' your std::vector when you declare it and it won't work. Any linter or having warnings turned on will catch these issues.
const doesn't make a difference in this case. It's about the passed-by-reference object being destroyed after the function returns. That is because the object was passed as a temporary.
The fact that you may know that make_appender definition being bad does not mean every C++ user knows as well. It's also impossible for you to know ALL the possible bad, UB leading C++ code out there.

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.

Not to disagree necessarily, but if you (give me a little rope) bucket languages that are hard to get past the compiler but more often correct when you do (Rust, Haskell), and languages where it's pretty easy to get something past the compiler and tweak it until it works well enough for your purposes (JS, C/C++), the tweak-it-until-it-kinda-works languages are fucking killing it on adoption.
I dunno man. I've done C, C++, JavaScript and TypeScript professionally for significant chunks of my career, and the trend that I've observed has overwhelmingly been towards stricter compilers. For example in the front-end world, TypeScript has absolutely exploded in adoption. Everyone could be still using JavaScript, but companies from startups to huge corporates have explicitly decided they want compile type safety -- often in "only" front-end code.

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.

[1] https://www.phoronix.com/news/Rust-v6-For-Linux-Kernel

Oh I think we probably see largely eye-to-eye. My weapon of choice when there are no other constraints is Haskell, and one reason I really like Rust is that I can get a lot of the Haskell features I like in a highly-performant setting. Most of the C++ I maintain these days is in whole or in part generated by Haskell. And if I have to write something fast by hand and it doesn't need to link to stuff I need, I reach for Rust generally.

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.

Ha, Rust community is very energetic, but IMHO they largely put that energy to good use!

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.

> Most of the C++ I maintain these days is in whole or in part generated by Haskell

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.

> it's pretty easy to get something past the compiler and tweak it until it works

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.

> the tweak-it-until-it-kinda-works languages are fucking killing it on adoption.

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.

JS and C++ are killing it on adoption because they had near monopolies for an extended period of time in their respective areas (browser, native higher level language). They are popular despite their obvious (some in hindsight) shortcomings due to lack of alternatives in the same categories.
Javascript‘s adoption has much less to do with its language features but with its unique positioning.

There was (and still isn’t) a competitor that can compete on the same level with Javascript in the browser.

Literally anybody can turn on warnings and heed the results.
You can add [[clang::lifetimebound]] on the suffix parameter and you get

  <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});
You dont need annotations, cppcheck already warns with:

    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
As awesome as that is, cppcheck doesn't seem ready for real-world use. Literally the first invocation I ran resulted in this error:

  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]));
                                                 ^
A function returning a value that depends on the lifetime of the function's parameter is not crazy at all. Every class getter method that returns a reference to a member of the class does this.
Sure. But returning the address of a stack-allocated object is (usually) broken.

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.

> returning the address of a stack-allocated object is (usually) broken.

What matters is the lifetime, where the object lives is a rule of thumb for guessing lifetime that results from C++ trauma.

Hint: getters are code smell.

Getters that return references are code stink.

I was trying to be a little more diplomatic in my sibling reply because I've locked horns with the Rust community before and not enjoyed it, but you're not wrong.
It's possible the author actually made this mistake, and because c++ is C++ they didn't realize why/how. Not a lot of programmers have a good handle on c++, maybe even most don't... Hence these other languages.
If the author deliberately chose to compile with warnings turned off, in order to present an example that would crash, then that tells us more about the author than about the point.
Totally not the point of the article, and totally subjective I know, but to me the thing that jumps out is how much more readable the Go code is than the others.
This is the main reason that I use Go as my main language, and why many orgs are starting to adopt it: it's easy to read and jump into. I would argue, however, that it's very easy to create antipatterns and just general spaghetti code with Go. A language that's easy to be productive with != one that's also easy to maintain. Design and philosophy becomes very important with large codebases in the language.

source: consultant, seen some truly heinous Go monoliths.

There are definitely some ways to do bad designs in go, but i have the feeling it will be more immediately apparent what's wrong (or at least what part of the system needs rework). The reason being that there are no ways to obfsucate an awful design by wrapping it on mountains of generics programming and language sugar, making the whole thing a lot worse.

It's only my gut feeling, but does that match your experience ?

Definitely subjective. I don't find some parts very readable. E.g., this line took my a minute to parse:

  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.

I don't write Go but it might have something to do with this:

https://go.dev/blog/declaration-syntax

Go's type syntax is unusual but supposedly much clearer when things get more involved.

I agree. However, my gut reaction was that the style of the go code was written differently than what I’d expect if asked to work off the rust version.
Can you mention the difference that made Go code is more readable than C++ and Rust?
> Can you mention the difference that made Go code is more readable than C++ and Rust?

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.

comparaison

Probableur a typeaux?

Was this posted by a French speaker? Because in English it's "comparison", not "comparaison"...