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.
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.
... 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 :/)
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.
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.
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.
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.
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.
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.`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.
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.
type ArcWrap<T> = Arc<Mutex<Whatever<T>>>
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`).
>Arc<Box<Foo>> ugliness
Box is not needed, Arc<Foo> should already be heap allocated.
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*.