Hacker News new | past | comments | ask | show | jobs | submit login

Couldn't you achieve the same thing in C++ by using message passing between threads instead of shared memory? That way, you would get safe parallelisation without having to fight the borrow checker in every part of the program. I understand that having safety statically enforced at compile time is a real gain in terms of eliminating that class of bugs. However, the mental cost a Rust programmer pays for that trade (due to borrow checker) seems too high for me if you can get the same thing simply by shifting your programming patterns slightly.



Couldn't you achieve the same thing in C++ by using message passing between threads instead of shared memory?

I know that the parent post used multi-threading as an argument in favor of Rust. But there are many ways to blow of your foot in single threaded C++. For example, consider this single-threaded code:

    std::vector<size_t> v;
    v.push_back(1);
    size_t *first = &v[0];

    std::cout << *first << std::endl;

    // The STL vector implementation will probably
    // reallocate the backing array at least once.
    for (size_t i = 2; i < 1000; i++)
      v.push_back(i);

    std::cout << *first << std::endl;
You wouldn't be able to introduce the same error in Rust. Once you borrow the an element of a Vec immutably, you cannot create mutable borrows during the lifetime of any immutable borrows. So the compiler would reject pushing new elements.

However, the mental cost a Rust programmer pays for that trade (due to borrow checker) seems too high for me

The thing is that most of the errors that the borrow checker will point out are potential programming errors in C or C++ as well. However, current C/C++ compilers will put most of the responsibility completely on you (plus tools like Valgrind). So, to me C++ has at least the same cognitive load if you want to program responsibly.


I agree with the general principle, but programming in Rust is not "program in C++, then fix errors". You really need to lay your code in away that will satisfy the checkers. That or jump into unsafe mode.

A good example of the challenge is that there's unsafe inner code in rusts own container code. If you try to build your own custom containers (or things like arenas) you'll likely work with unsafe code... Or rely on rusts container code (not a bad thing of course).

My feeling is that "C to Rust" is close to "Java to Haskell" more than anything else. The extra layers are important enough that they need a new thought process. (I say this as a proponent of rust)


Same thing is also true in C++, the stdlib also uses "unsafe" pointers inside, but you can use unique, shared etc.

> You really need to lay your code in away that will satisfy the checkers.

There are some patterns that are common but problematic but are being addressed by non-lexical lifetimes. Other than that it's almost "program in C++, then fix errors".


You really need to lay your code in away that will satisfy the checkers.

But that becomes a second nature after a while. I think all the edge cases that still exist make it more problematic. E.g. I was writing some SIMD code yesterday with a loop somewhat like this (simplified):

    let mut v = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
    let mut s: &mut [f32] = &mut v;
    
    while s.len() >= 2 {
        println!("{:?}", s);
        s = &mut s[2..];
    }
Playground: https://play.rust-lang.org/?gist=104348b0edb96ca0f68d701373e...

It makes you scratch your head for a bit.

A good example of the challenge is that there's unsafe inner code in rusts own container code.

I am pretty proud that one of my first small Rust projects was a randomized ternary trie. Apparently, it does not use unsafe anywhere.

https://github.com/danieldk/rtrie/blob/master/src/ternary.rs

Not that I am proud of the code ;).

But in all seriousness, unsafe in container code is somewhat expected, because at some point you need to wrap raw memory (as e.g. in Vec). I think quite a bit of, but probably not all, unsafe use in containers such as Vec comes from that.


> However, the mental cost a Rust programmer pays for that trade (due to borrow checker) seems too high for me if you can get the same thing simply by shifting your programming patterns slightly.

i remember reading about the stylo (multithreaded css styling) engine, something that has been tried times and times again to implement it in C++ without success: too complicated. sooner or later you'll get too-hard concurrency bugs and the project would be abandoned. and it wasn't just mozilla with firefox, google also failed to implement concurrent styling with chrome.

the blog post stated that this time they were successful _only because of rust_. the mental overhead of managing the borrow checker is far lower than the mental overhead of dealing with concurrency issues in a complex high performance project.


Mozilla was full of talented C++ programers.

They thought it worthwhile to invent a new safer language for massive parallelism and security, rather than to attempt it in C++.

Also coming from Java, the Rust experience is not that painful. It's like learning a completely foreign language.


I think you misunderstood Rust. There’s no additional mental cost compared to C++. In C++ you must do thing the same way (keep track of references, variable lifetimes) in order to write any sane program. Rust just help you to declare those things and ask the compiler to enforce it. And no, message passing is not the savior to parallelisation.


There is definitely some mental cost. By making lifetimes explicit there are additional barriers in the way of doing certain things that are safe but involve invariants that are too high-level for rust's semantics. For example, taking mutable references to multiple elements of a container simultaneously.


.split_at_mut(), there you go. But I understand that there are cases when Rust rejects correct program, that is where we need unsafe to build safe abstraction. Come back to additional mental cost, it’s just the same debate between static & dynamic typed language. Either you write more, specify constraints, and have the usage checked or you write less, imply your constraints and must satisfy the constraints of usage by yourself (or with helper tools/tests). To me that’s just “when” I have to spend the mental resource, and the total cost is roughly the same. But then, knowing the separation helps to choose which tool for which type of project.


I don't think this is just about being more or less verbose. It's about the awkward ways it contorts the ways you write programs. For example, in C++ you might write an asynchronous job scheduler with an API like the following:

void Scheduler::schedule(const Job& job, Result* result);

void Scheduler::wait();

The consumer of this API in C++ has an easy job. They can create an array of results and iteratively call schedule(jobs[i], &results[i]) for each job, and then call wait(). A similar API in Rust would be a giant pain in the neck to use, because there's no simple way to collect the results. You'd likely want to scrap this API entirely and have Scheduler expose some kind of queue or something.

My point is that these extra constraints are not just extra typing locally to specify how you're using lifetimes. You'll likely have to rethink the way you write APIs and whole programs in Rust because of the ways it restricts mutability. Which is OK, it's a trade-off, all I'm saying is the safety doesn't come for free.


While there might be all kinds of troubles _implementing_ that API, I don't think it wouldn't be that hard to use. You would need to have a valid "not ready" state for the results, because Rust requires that they are in some state at the moment they are constructed. From the lifetime perspective, the data structure where the results live should outlive the schedule calls – but that isn't a problem if you construct the array first. There will be a lifetime requirement but that's it.

The mutability thing isn't a problem. You can allocate a vector of results and jobs, and then have a mutable iterator over them, zip them together and then iterate over that. There's no problem passing mutable references to each of the elements.


The problem is that schedule() needs to mutate result asynchronously at some point in the future. It can't do this if result is a mutable reference, since it needs to return immediately so borrowing a reference doesn't work. It also can't take ownership of result, since it has no way to give it back (short of changing the API, for example by making wait() return a collection of results).

The right way to implement the API above is for schedule() to take Arc<Mutex<Result>>, and explicitly share mutable ownership over each individual result between some unspecified asynchronous execution mechanism and the caller of schedule(). This makes the calling code significantly more complicated as it needs to incur the overhead of atomic reference counting and a mutex per result, and must try to lock each result before using it, even if it knows there are no other owners of the result due to wait() returning.


I don't see the problem with returning immediately after borrowing a reference, if the scheduler lives shorter than the results (if it doesn't, holding the reference would be unsound): https://play.rust-lang.org/?gist=226f92bc311dbac05c112b10ba4...


You're exclusively borrowing res_a, res_b and res_c for the lifetime of the scheduler. Which works fine since res_a, res_b and res_c are hard-coded variables with separate lifetimes. What rust doesn't know how to do is borrow mutable references to elements of a container without borrowing the entire container.

https://play.rust-lang.org/?gist=1324a919ca5d0e2f16e445364a7...


This can be made to compile by using iterators: https://play.rust-lang.org/?gist=33d86898c4d16bf9bc9023a9c9a...

Rust also offers some other methods to allow multiple mutable references to containers: .split_mut() comes to mind – that splits a mutable slice into two mutable slices that don't overlap. In the future I'd like to see in the stdlib "container adapters" that take in a normal container and provide an interface to it that allows taking multiple mutable references while ensuring the soundness using dynamic checks (it can keep an array of the pointers or indexes that are borrowed out, and check against that). I created such an interface as an experiment last year: https://github.com/golddranks/multi_mut


The sibling comment shows how to do that in safe Rust code. But then, even when you have much more complex control that Rust compiler can not prove safety, you can still resort to unsafe, build your algo, and wrap it into a safe API, just like `split_at_mut`


This cuts both ways. Because I can offload checking from my brain to the compiler, I feel personally that the mental cost is actually reduced overall. YMMV.


IMO the cost is worth it for the documentation alone. What lifetime a callee expects me to uphold for the arguments I pass is very often a mystery on which the docs are absolutely silent in C/C++. In Rust, it's part of the type signature and enforced by the compiler, so I can always read it off at a glance. This is a huge win in reading a new API.


If you’re sure they’re safe then just put them in an unsafe block and wrap that in a safe api.


> However, the mental cost a Rust programmer pays for that trade (due to borrow checker) seems too high for me

In C++ land, you'd have to use TSan thread sanitizer and a lot of other tooling (ASan, Valgrind, etc) to achieve even close to the same level of confidence. Not only is this a runtime check but all that tooling has a fairly heavy mental cost too.


No, you can't. It's either performance or bugs – look at the big C++ projects.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: