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

I’m a pretty good C++ developer and have been trying to use Rust for a new project.

The good:

* Cargo! It’s amazing, thank you.

* Portability. The build model is the same between different platforms; all the time I waste managing multiplatform builds with C++ instead goes to coding.

* Libraries. C++’s stdlib has gotten a lot better, but it still can’t parse json or make web requests or create zip files.

* Community. The vibe from docs and blogs and videos is good, cheerful and optimistic.

The bad:

* The borrow checker. Memory safety is great but there are plenty of memory-safe designs that the borrow checker complains about. With modern C++ and sound design techniques, memory safety is not something I worry about much in my C++ projects, and the static analyzer proves me out on that. In particular, a typical application design involves some variation of the Observer pattern (implementations can vary wildly), where views read/“observe” models owned by controllers. Why should I beat my head against the wall with a systems language? There should be an escape valve that doesn’t sacrifice memory safety.

* Documentation about the borrow checker and lifetimes. The Rust book tries to treat this in an approachable manner, but I just wind up more confused; I don’t think it can be treated casually and as such I’d like to have a very thorough and detailed description of what -exactly- is going on with these language elements. If Rust’s going to be a systems language, it’s going to need to get comfortable describing what’s happening precisely.

* dyn. I love templates, static polymorphism is great. But sometimes you need runtime polymorphism. Rust treats dyn like a second class citizen.

* Arc<Box<Foo>> ugliness. Heap-allocated reference counting is a useful thing sometimes. I know Rust doesn’t prefer it, but Rust’s FTFY attitude here is annoying.

* Where are the functors??!?!? Lambdas are great and all, but I can’t create unbound trait/struct functors and then invoke them on strict references later? This is a huge limitation in runtime flexibility for application development. Boost::function and boost::bind worked with VC6 twenty years ago! When I realized Rust didn’t have functors, I became much less interested in the language.

* No function overloading. This is just silly and onerous. Rust could create sensible restrictions to avoid ambiguities and C++’s ADL/type coercion complexity but there’s nothing ambiguous about having two functions with different arity sharing the same name. The hardest problem in CS is naming things, and Rust’s lack of function overloading makes it harder still.

In general, Rust seems a whole lot less expressive than C++, and the claims that these restrictions are sacrificed for memory safety are bogus. Nothing above involves pointers and reinterpret casting and void*.




> Libraries. C++’s stdlib has gotten a lot better, but it still can’t parse json or make web requests or create zip files.

... but rust cannot do this three things either from just the stdlib? I don't see anything related to requests, json, or zip files in here: https://doc.rust-lang.org/std/#modules

otherwise if you can install libraries, json, zip and web requests are just one vcpkg / conan dependency away in C++ too - and if you want to limit the amount of dependencies you can just use boost which in 2022 does support json and web requests, and can compress / decompress gzip (not zip though :/)


> * The borrow checker. Memory safety is great but there are plenty of memory-safe designs that the borrow checker complains about. With modern C++ and sound design techniques, memory safety is not something I worry about much in my C++ projects, and the static analyzer proves me out on that. In particular, a typical application design involves some variation of the Observer pattern (implementations can vary wildly),

Don't you still have Iterator invalidation to keep track of and I thought I saw posts regretting using `std::string_view` because it is easy to reference deleted memory.

Anecdote: I maintain a template language library for Rust. I saw the potential for it to speed up if I used more borrowed data (like `&str`, Rust's version of `std::string_view`). I gave it a try and once it compiled, it just worked without crashes. I reflected back on if this was in C++ and the conclusion I came to was that maintainable code is a much higher priority than performance and that any future change in a similar C++ code base would require global analysis to make sure it was safe, making the performance gains not worth the lack of maintainability. In Rust, its been trivial.

> Where are the functors??!?!? Lambdas are great and all, but I can’t create unbound trait/struct functors and then invoke them on strict references later? This is a huge limitation in runtime flexibility for application development. Boost::function and boost::bind worked with VC6 twenty years ago! When I realized Rust didn’t have functors, I became much less interested in the language.

The fact that you can't `impl Fn` bothered me for a long time. It was one of the many examples of where Rust felt unfinished.

Recently, I've seen an inverted pattern for this, define a trait and `impl MyTrait for Fn`. I've found I much prefer this pattern over the cases where I would have used a functor. Granted, there are more ad-hoc cases where functors would be better.

> No function overloading. This is just silly and onerous. Rust could create sensible restrictions to avoid ambiguities and C++’s ADL/type coercion complexity but there’s nothing ambiguous about having two functions with different arity sharing the same name. The hardest problem in CS is naming things, and Rust’s lack of function overloading makes it harder still.

I thought I'd miss this but it hasn't been as bad as I expected.


> Don't you still have Iterator invalidation to keep track of and I thought I saw posts regretting using `std::string_view` because it is easy to reference deleted memory.

I don't run into iterator invalidation that much in practice. I'm not often erasing elements from collections. There are all sorts of things like std::string_view where you could reference bad memory if you don't manage lifetimes, but I... just... come up with designs where lifetimes are managed properly? It's probably habit and very intuitive for me, hardly conscious of it.

Also, this is where the static analyzer is fantastic. If you have good unit test coverage, it will let you know you've screwed up and pinpoint where.

I don't mind the borrow checker as a default, but it would be nice for an escape hatch that wasn't an unsafe{} block. Perhaps one with runtime checks? Predictable conditions are virtually free with branch prediction.

> Recently, I've seen an inverted pattern for this, define a trait and `impl MyTrait for Fn`. I've found I much prefer this pattern over the cases where I would have used a functor. Granted, there are more ad-hoc cases where functors would be better.

Can you give an example or provide a citation to this? I'd love to read more.


> Can you give an example or provide a citation to this?

It would look something like this:

    trait MyOperation {
        fn run(&mut self, a: T, b: U, c: V) -> R;
    }
    impl<F> MyOperation for F where F: FnMut(T, U, V) -> R {
        fn run(&mut self, a: T, b: U, c: V) -> R { self(a, b, c) }
    }

    fn my_api<F: MyOperation>(f: F) { ... f.run(a, b, c) ... }

    struct MyFunctor { ... }
    impl MyOperation for MyFunctor { ... }
    my_api(MyFunctor { ... });
That is, when defining `my_api` and you would like to accept both lambdas and functors, instead of using a `Fn` trait bound directly, use a more specific trait which has a blanket impl for lambdas.


> Can you give an example or provide a citation to this? I'd love to read more.

`nom`'s `Parser` trait is implemented for functions. You can mix and match parser functions and parser types, including the built-in types that are returned when you call the post-fix combinators on the `Parser` trait.

https://docs.rs/nom/latest/nom/


I've said many times that C++'s tooling is a dumpster fire of epic proportions. No-one that wants to use the language should have to spend a non-trivial amount of time a) downloading a build system in addition to the compiler and linker, and b) reading the docs of the build system they've chosen.

It's the compsci version of Hawking's quote 'for every equation I include in this book, I'll lose half the readers' (or words to that effect). For every extra line of setup, you lose some of your potential audience.

Just setting up C++ tooling requires non-beginner level skills.


> setting up C++ tooling requires non-beginner level skills.

I've taught C++ beginners to use a C++ compiler, linker and make with no problems, and earned quite a bit of money while doing so.


You are right. I have spent a lot of time tinkering with build systems on sizeable projects to be sure I do not want to do it myself.

But why would any beginner do not do the same thing that Rust offer: use one written by someone else? It does not have to be special or even open. Beginner needs one that they can learn from other people around them. Most of corporate ones are not even documented. People onboard by watching over a shoulder how it works xD.


Concur. I love C++ and I hate this aspect of it so much.


I'm by no means an expert, but I don't see a lot of your "bad" points, they mostly seem like gripes about not being able to use Rust exactly as you are used to in C++ as opposed to pitfalls of learning the Rust way and doing it with Rust patterns, which would be true any time you move from one language to another.


This is a pretty common complaint from people who have used C++ forever and don't want to give up their design patterns when learning a new language. "Not expressive" is self-serving way to say alternatively "I can't write my OO soup" or "I don't care about safety in my C++ code", which are both evidenced in the OP's comment.


I care about safety in my C++ code, thanks. What I'm saying is that all of the boogeyman stories about terrible C++ code generally don't apply to modern C++ if you work within its idioms -- memory corruption bugs are few and far between, and easy to find and fix.

I've certainly seen truly awful C++ codebases, full of undefined behavior and stack and heap corruption. It's possible to write very bad unsafe C++ code; it's also possible to write safe C++ code without much difficulty.

As far as design patterns: fair point that design patterns and idioms may be different in Rust but... what are they??? Fundamentally, in an application, some parts are going to read some data while another part owns it. How does that work in Rust? I haven't seen good answers yet. With procedural information processing utilities those concerns don't come into play as much, but I'd love to read about the architecture of well-functioning Rust GUI app.


> What I'm saying is that all of the boogeyman stories about terrible C++ code generally don't apply to modern C++ if you work within its idioms -- memory corruption bugs are few and far between, and easy to find and fix.

I think people are increasingly frustrated by this refrain, because there's empirical evidence that suggests otherwise. Now, it's entirely possible that you are starting greenfield projects with only super senior engineers in domains where a few safety critical bugs just don't matter, and in that case the learning curve required for memory safety just isn't worth it. That's a business decision you can make, and I wouldn't judge for a second. There are always trade-offs in engineering. However, "I don't write bugs" just isn't credible in 2023. I'm sorry if this seems uncharitable, and I appreciate that you're making a more qualified claim, but safety is not a "boogeyman."

> I'd love to read about the architecture of well-functioning Rust GUI app.

GUI is certainly a still developing area in Rust and has a long way to go. It's easily the weakest part of the ecosystem. That being said, it's still totally possible to build GUIs in Rust. The most common architectural pattern would be the Elm architecture, which avoids many of the issues of ownership that more traditional OO patterns introduce. I've also had a lot of success recently using immediate mode libraries, although obviously this won't work for every application.


> In particular, a typical application design involves some variation of the Observer pattern (implementations can vary wildly), where views read/“observe” models owned by controllers. Why should I beat my head against the wall with a systems language? There should be an escape valve that doesn’t sacrifice memory safety.

The escape valve here that doesn't sacrifice memory safety is reference counting and perhaps interior mutability. (And this point certainly does involve pointers, if not void*.)

> I can’t create unbound trait/struct functors and then invoke them on strict references later?

You can't implement the `Fn` traits for your own types, but this shouldn't have any impact on what you can express, should it? If you're going to pass in references at the call site you can do that just as well with lambdas (or if you really want to use your own type, with a hand-rolled trait to replace `Fn`).


Fair points. I especially agree about `dyn` being second class, and `Arc<Mutex<` being verbose.

Not exactly sure what you mean about functors though. Maybe an example would help.

No function overloading was definitely a good decision. You can emulate variadic functions by implementing traits for tuples.


I agree it's a little verbose, but in those cases I usually make my own type aliases:

type ArcWrap<T> = Arc<Mutex<Whatever<T>>>


I don't disagree with your overall sentiments, however

>Arc<Box<Foo>> ugliness

Box is not needed, Arc<Foo> should already be heap allocated.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

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

Search: