456 karma · joined July 24, 2021
I have been using C++, Python and Rust professionally, the developer experience is simply one order of magnitude better with Rust. The tooling is excellent, a sane compilation model brings you a lot, the type system is very helpful. Compared with C++, I measured x3 productivity on writing initial code on a project (sometimes C++ was the rewrite, sometimes Rust was, so not this kind of bias). Compounding maintenance, I expect it to tend to 1 order of magnitude in time.
Meanwhile, using Python can sometimes feel magical, but I have to maintain a library written in the language, and I can feel that it hasn't been optimized for this use case. It is terribly hard to keep backward compatibility, adding typing annotations is somewhat useful, but has terrible ergonomics, ensuring no regression is a pain that translates to endless suites of unit tests even for trivial matters ("be your own compiler" will never be a great idea IMO, although tooling like pylance does help a bit).
Meanwhile the rearguard is defending for new projects some kind of "No true Scotsman"ish modern C++ delusion, that I am still looking for and that moves like goalposts in every discussion I have with its proponents. My only explanation for the phenomenon is that it is fueled by hubris and sunk cost biases, because my experience with the modern idioms is that while they bring significant improvements, they are still very subpar compared to the expectations brought forth by a modern language, and memory safety related CVEs are still being written everyday in C++.
So, what should I do? It is true that, in more than 15 years of coding, Rust represents a revolution in programming in my eyes. Should I sit idly while others are missing on it and someone is wrong on the Internet?
- exceptions don't appear in the signature of a function
- you don't have to handle exceptions explicitly, while you cannot access the value of a Result without explicit error handling
- Exceptions are implemented like Rust's panic. The error path in exception is more costly, while the nominal path is supposedly less so, due to less register and branch pressure than the result type that at least has a discriminant to check.
Exceptions are such a different way or handling errors from Result that my team finds itself reimplementing Result types in C++ again and again. Alas, it is neither standard nor ergonomic in C++
result.unwrap_or_else(|| todo !())
(unimplemented! is for missing functionality in a given version, todo! is for missing functionality during development)But using unwrap for these kinds of TODO is ok, code review won't let unjustified unwraps to pass
Anyway i wouldn't say "plenty", but i did came across crates (parsers :/) that would unwrap on malformed input. the workaround is to encapsulate their use in a catch_unwind.
for the record, i had a similar issue in a c++ lib where the author elected to abort on the unsupported input, so i'm somewhat thankful that the idiomatic mechanism is panic (which is recoverable if needs be) in Rust
You can't use smart pointers exclusively, because there is no "smart observational pointer", and there cannot be one, because that would be a reference type checked by a borrow checker, which is exactly what rust uses.
You can emulate it with shared pointers, but then you're left with basically a reference counted language. Why not using swift at this point? Also this creates a semantic impedance mismatch that tends to manifest with cycles.
Source: walked a codebase that would (ab)use shared pointers
Even if it were, most code isn't "fire and forget", and so for most code the cost is dominated by maintenance cost, which has more to see with reading than writing code.
Languages that are concise are so because they either express information very densely, or express less information (via for example ignoring error handling or making it implicit with exceptions). In practice I find that such languages are much harder to maintain, because the missing information has to be rebuilt by the reader.
C++ was out in 1985, and qt's first version wasn't until 1995, with kde starting in 1997. And these were simpler times.
I'm very not convinced that lack of GUI is a rust specific problem
Indeed. IME (around 10y C++, mostly c++14 and c++17, and 6y Rust), I measure x2-x3 velocity in Rust, a lot of which comes from the tooling, a part of which from ub-freedom and much easier and safer parallelism, and the rest from the expressive type system and functional polish.
This is for initial development. Factoring in the maintenance over the years I suspect it tends to an order of magnitude of difference.
The guardrails make it harder to shoot the proverbial foot, but also to get code to compile at all.
Well, at least at the beginning. One becomes used to the so-called "compiler-driven development", at which point it becomes a breeze.
- no way to express "lifetime relationships", they remain in comments or force you to work around the problem by using copied objects or worse, shared pointers. This resulted in very real bugs in our code due to a library returning structs that had to live shorter than some other object, without it being clear from the name of the struct. Coincidentally, this same library has a rust implementation, where keeping instances of this struct for too long is a compiler error.
- no blessed way to express thread safety in the type system
- terrible build system without a blessed package manager, which is not about to change because:
- they are still headers everywhere. While modules are technically in C++20, tooling is not ready today (save for MSVC). The module implementation is also dubious in that it seems to cater to any niche use case in existence which adds a lot of complexity (and new opportunities for ODR violations...), instead of going "clean slate" and providing a simple system (preventing the split between declaration and definition and not being orthogonal to namespaces would have been a good start)
- no ergonomic sum types. Sum types are very important to model business logic and it is hard to go back to languages lacking them. There are discussions of pattern matching for C++ but it's not in C++20.
Several of these issues are impossible to fix in an existing language without breaking an unreasonable amount of existing code, so I don't see future versions of C++ reversing the statu quo. I also don't really trust the committee to achieve simple, ergonomic solutions, when I look to eg what was done with modules or coroutines (here I heard that the solution is to use third party libraries, but while it is tractable in Rust it is much less so in C++ due to the lack of satisfying tooling).
You're welcome to elaborate if you disagree though.
Also, nothing in the post lets us infer that the author doesn't know C++11 and later.
C++ does provide tools to build scalable software, and was already doing so in 1998 (few new features on this front since 98 I guess? Maybe modules, but using them for portable development in 2022 looks difficult given their state in compilers that aren't MSVC). I don't think this is a reason to prefer C++ over Rust (if anything, the tools for encapsulation and modularization provided by C++ are way less convenient and efficient than Rust's--speaking from experience of C++14 and Rust here). Also C++ has the massive drawbacks of lacking ADT, forcing headers on users (which is elaborated in the post I believe) and not offering a "safe mode" like Rust does (the tooling is also massively better, but somehow cargo felt like a hindrance to the author of that post, so there's that).
It did surprise me that the author wouldn't elaborate on why C++ was not chosen, when her argument made it look like it could provide some of the advantages she found in Rust. However, she doesn't *have to* elaborate on every possible subject in that post, and implying she doesn't know the language (anymore) because of her not focusing on the reasons she didn't use it looks like a dubious stretch.
You need shared references for your DB, implying you need interior mutability. This is how Statements are implemented in real-world rust database drivers such as rusqlite (any operation on a db is done through a shared reference). The fact that a very real package is doing it proves that the pattern you're talking about is, in fact, possible.
In practice, this makes vulnerabilities in eg. argument parsers (like the recent "baron samedit" vulnerability in sudo) incredibly unlikely.
Axum is very young (was announced <1y ago), it has good momentum, is based on hyper and built by the tokio team. On the other hand, it has few actual projects built with it, I couldn't find a guide for it like actix or even Rocket have (it has some documentation in its docsrs documentation, but it is pretty minimal). Crucial questions like how to handle configuration, integration with database, autoreload, are left to the examples at best.
I have no doubt that these things will come if the framework matures. I'm just questioning if it should be the default choice right now, as opposed to in a year.
(Btw, I would absolutely recommend against Rocket right now, even though API wise it clicked the best with me, until it gets more maintainers and development resumes)
Looking at the repository, it looks like the output is a bunch of extern "C" functions, though. Does that mean that one must call eg destructors by hand? How are C++ exceptions handled? Is this sound in the presence of non-relocatable types (eg std::string)? If so, how? Also, this only handles the C++ -> Rust direction, right?
Anyway, thank you for letting me know of this project!
It is very actively developed though so I trust that we'll get there eventually.