456 karma · joined July 24, 2021
This introduces a whole bunch of design issues, but isn't `panic` (in unwinding mode) basically C++'s exceptions, implementation-wise? Couldn't we use `panic` + `catch_unwind` as a poor man's exception system, should the performance situation really require so?
Also, could we maybe add an attribute `#[exceptional]` to enum variants in a match, that would result in a match implementation closer to exceptions, implementation wise?
C++20 is adding a module system to C++. That's a major change to the compilation model. It is also adding concepts, a major change in the way templates are to be written. The very article we're commenting on is discussing how exceptions should possibly be replaced by another error report mechanism, several proposals are in flight for this. There's also the "destructive moves" proposals that have the potential to change a lot how we write types.
A telltale sign that these changes are major is that the entire std has to be "modularized" to support modules, and to be modified to support concepts. Similarly if exceptions are to be revised a large chunk of exception using functions from the std would need to be modified.
On the Rust side, I think the only change that has even a comparable impact is const generics (and maybe specialization).
Existential types and GAT will change how to express some traits (allowing a LendingIterator for example), but I don't expect they will affect a large portion of Rust's std.
Also of note is that the Rust changes come to add new systems orthogonal to the existing features (const generics fill an obvious void compared to C++, same with GAT and existential types where Rust's expressivity is limited in comparison with C++ atm). By contrast in C++, the module system comes to replace the headers, and a change to exceptions would replace the current exception system, creating churn.
Case in point, here is the same race condition in Rust:
- playground[0] (see for yourself)
- inline code (not that long):
use std::thread;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
static COUNTER : AtomicU32 = AtomicU32::new(0);
fn increase() {
for _i in 0..100000 {
let a = COUNTER.load(Ordering::SeqCst);
COUNTER.store(a + 1, Ordering::SeqCst);
}
}
fn main() {
let mut threads = Vec::new();
for _i in 0..400 {
threads.push(thread::spawn(increase))
}
for thread in threads {
thread.join().unwrap();
}
println!("Final counter: {}", COUNTER.load(Ordering::SeqCst))
}
The data races that Rust prevents are "instruction-level data races". They occur when a single, non atomic, architectural instruction is executed in parallel by multiple threads. In that case we can really get corrupted values, in that "partially written" value in the architectural sense (depending on architectural details) can be produced. Think a double word with one word rewritten by one thread and the other word rewritten by the other thread.In the given code example that is not an instruction-level data race, the produced value will strictly be in the range <max_number_of_increase>..<max_number_of_increase>*<number_of_threads> (plus or minus the usual off-by-one error), and no architectural artifacts can ever manifest.
I guess the insight of the article could be that `counter = counter + 1` is closer in Python from `a = atomicLoad(counter); atomicStore(counter, a + 1)` than from `atomicIncrement(counter)`?
I wonder if using `counter += 1` changes anything? In any case, I can see how it is easier to trigger accidental race conditions of that kind in Python than in Rust, with the atomic operations being implicit in the former, and explicit in the latter.
[0]: https://play.rust-lang.org/?version=stable&mode=release&edit...
I don't have a similar evaluation for C <-> Rust, but I suspect the result would be even more skewed in Rust's favor: sure, you might be writing individual lines faster in C, but the lack of iterators, generics, traits and RAII, and the relative difficulty to use dependencies means that you will waste a lot of time reinventing the wheel, often suboptimally.
While true for C++ which is not very interoperable, C being very interoperable means that there is no advantage into using a C API from C rather than from Rust.
> how security critical your application is.
Rust also has productivity and general "correctness" advantages that go beyond security (although yes that alone is a factor)
Now, embedded is a special case. You need platform support, and possibly certification, none of which are a given in Rust today. Arguably, not in C23 either at the moment, though.
As such, it has "missing parts" that I think are important:
1. Reasons to intentionally panic are mainly for invariants that you failed to express through the type system, and so that have to be checked at runtime, although you "know" that they should always be true. An example of this is having a struct implementing Drop while still having a method of that struct that allows to take its internals. To achieve this you typically need to put the internals in an Option, and have drop only drop if the option is there. Since all methods can only be called when the object has its internal in place, you should unwrap the internals before using them in each method. Returning an error in this case would be exposing internal architecture details and essentially "dead code".
2. You can recover from a panic. This is especially useful if one of your dependencies is misusing panics or you're triggering something that "should never happen". You may want to use that for a longer-lived application (eg a web server), so that a panic in one connection does not bring down the entire server. This is also necessary before calling into FFI because unwinding in another language is UB at this point.
3. When writing applications, `anyhow` is a great crate for "stringly typed, user-facing" errors. Its superpowers are to convert any error into its erased error type, and to be able to add context as a String to any error. You can then iterate through the various layers of errors, which is great for giving context to the end-user of the application. You shouldn't use anyhow in libraries though, because otherwise you will expose "stringly typed" errors everywhere, that are hard to act upon for your programmatic users.
Error handling is a difficult topic in any language. Rust's error handling story is better than in many languages, but I feel it still lacks some things:
- I would like to be able to mix and match error types without making them "stringly typed" and more freely than with the rigid "from" implementations using thiserror
- The current system pushes people to make a single big Error enum for their crate, which obfuscates which errors can or cannot be returned by each individual function, and so makes it harder to act upon each error condition. I try to 1. have one specialized enum per function (or per functions with similar errors) 2. have one "generic" enum containing all of the others, so that if the user doesn't care about the specific error they can just `.into` their way to the generic error.
- The current system is very "type" oriented, and so doesn't push people to actually model "error conditions". eg multiple error conditions can be caused by IoErrors, but it is tempting to snuggle all IoErrors in a single enum variant "IoError(IoError)", whereas a more expressive way would be to describe error conditions: eg "CouldNotOpenConfig(IoError)" and "CouldNotSaveData(IoError)" instead of a single "IoError(IoError)".
The article remains a good short introduction to error handling in rust, but I wonder if we *want* a short introduction to a difficult topic.
Not necessarily. Many C programming errors invoke Undefined Behaviour, and then "appearing to work correctly" is a permissible option. So you will miss the programming error, until a change seemingly unrelated to that part of the system will trigger some segfaults or wrong results, or allow a determined attacker to use your program to launch calc.exe.
A better C++ rust interop story is a critical piece for Rust's success IMO
Results will depend on the available ecosystem for each language for the task at hand. For instance I'd still choose C++ with qt for writing a GUI today.
The reasons why Rust is more productive are easy to understand and touch most of the development steps:
- building is trivial in Rust, adding dependencies also. Depending on how broken the CMake of your C++ dependency is, it can take several days to wrap it so it can be used sanely. The remaining time is dependency review, which must also be taken in C++.
- Development itself is much easier, thanks to Rust's expressive typesystem (mostly sum types and traits) and "functional-lite" iterators (C++ is getting there with ranges but today we're still far from the same ergonomy, we're still missing pattern matching, and views introduce lifetime issues). The sane error/warning demarcation allow to develop without the equivalent of "-WError" (which is hard to do sanely in C++ due to eg missing return being a warning), which allows to use these warnings to drive development (you can stub every function and when you no longer have any "unused variable/unused function" warning, you're done). More generally, Rust removes most of the boilerplate of C++: you don't need to maintain superfluous representations of your interface as headers (the documented interfaces are generated using cargo doc), you can derive `Hash` instead of opening the std namespace, functions and structs definition all start with a keyword so they're easy to grep for, the language being expression oriented means we don't have to rely on immediately invoked lambdas as soon as what we want is to bind a variable depending on an if, we don't have pure stupid boilerplate like the rule of five to enforce, and a thousand of other ergonomic improvements over C++. Rust feels like a language designed by a C++ developer tired of the flaws of C++.
- Since many errors are lifted at compile time, the feedback loop becomes much shorter. On the same library that has both a C++ and a Rust interface, we had memory errors several times due to a function that returns a view of its argument without that being clearly expressed or expected. In C++, this meant looking for what went wrong after a segfault, something that took an hour to two engineers. In Rust, the compiler told us that the particular variable was not living long enough, and went so far as to suggest the fix. Time spent on this: 10 seconds. And this comparison only hold for the memory errors that we did catch in C++...
- Test and documentation use standard tooling that is included by default. The ecosystem documentation is stored on a centralized website, meaning is a single place to look at to find documentation (that at least contains the interface of the library). - Review is made easier with no implicit code paths due to implicit conversions or exceptions. Error handling has the right amount of noise, a single character to pass back the error (possibly adding some context), otherwise a match construct to handle each error case. The error story is not perfect, but much better than in C++. The "dangerous patterns" are made easy to spot (unsafe, unwrap, etc) and should be confined to specific parts of the code (eg hot loops where benchmarks prove that it is necessary). Rust make code review able to focus on the problem at hand rather than the many rules you have to follow to not blow your foot off in C++. In the presence of other developers (or even for myself :-)), it is really welcome.
- Maintenance time is reduced by all of the above, over time, compounded by the ease with which one can refactor Rust code compared to C++ code. If refactoring a module without unsafe there is no chance of introducing memory issues. Generally the strong type system (no implicit conversions) makes it easier to not accidentally introduce logic errors
Now, there are drawbacks to Rust: the ease of adding dependencies means that we can have deeper dependency tree, so this make reviewing dependencies a bit more tedious (thankfully code review is easier). Occasionally the borrow checker comes in and forbid some patterns (mostly when we would write code depending on iterators not being invalidated in C++), and having done less rust than C++ sometimes I need to find the correct pattern for something. On average though, the advantage is very clear.
Exactly, on both accounts, thanks.
It strikes me as odd that GP would describe "iterating on performance" as "underperforming", unless GP is in a very specific domain where the performance requirements are always known in advance and where they never change. Getting the last drop of performance from code is a trade-off. It impacts the readability and maintainability of code. To get the last drop, it can depend on the shape of your expected input data, and it requires looking at the produced assembly and performance profiling results to find bottlenecks and act on them. This alone implies iteration. If you're telling me that your engineers know their compiler so well that they can tell in advance what assembly code will be generated when changing seemingly unrelated parts of the code, eg they know the inlining limits of their compiler, I call BS.
Iterating on performance takes time to develop and maintain, which is why it should be done just enough to meet the requirements, in particular in the hot loop and to reduce bottlenecks. If your developers are optimizing all the code they write to the ASM instruction, they are probably underperforming (unless specific domain where so little code is written or the target is so small that the benefit is worth the cost). Understanding that trade-off is part of seniority (aka juniors will often both "prematurely pessimize" by writing inefficient code without increasing maintenability or in the hot loop and "prematurely optimize" by avoiding allocation at all costs in CLI argument parsing).
What? Why? I've been developing C++ for 10 years and seen firsthand the problems it causes and that are addressed by Rust. If I start writing Rust I'm going to be more productive (from build system to maintenance), write more performant and reliable code faster, so I don't see how the delivered product would be worse, as I'd have more time to iterate.
About the rate, maybe that would be more expensive, but I'd say it's still worth it. Anyway, it's not like getting competent C++ developers is cheap either.
As for GP's message, I agree it looks unrealistic that "kid fresh out of college" would magically write performant code because they're writing rust. There is more to performance than the language, and it is particularly easy to write bad performance code in rust (and in C++ I must add). That being said, I'd still be less anxious to have a junior in a rust codebase than near a C++ one.
Requests is currently #6 in its Web programming > HTTP client category[0]. The download counts, number of contributors, version history, and dependent crates is also prominently displayed.
We have a root project that's fully CMake, that orchestrates our sub libraries (also using CMake, declared with ExternalProject_Add) so the dependees are built before their dependents (and also to handle final packaging). The subprojects are added as git submodules.
I tried VSCode and QtCreator but none of them seem to support CMake multi-projects defined in this way. I have to create a build directory per subproject, which can be painful when we have so many subprojects.
To retain usefulness, we need a superset of C++ that would include either a GC (at which point, why use C++), or lifetime-like annotations, and the need to actively migrate the ecosystem to it. At which point what you have is a rust-shaped language with an outdated compilation model, tons of pitfalls, an inexpressive type system, and a slightly better interop with C++.
Considering that such a language doesn't exist, that which features to remove and which to add will be source for endless discussions, I'd say that the move that isn't overkill is to use the language that actually exists today and delivers on these promises. Rust isn't overkill, despite 10-years of C++ experience I'm way more productive in Rust, be it while writing code, fetching dependencies, reviewing code, and of course all of this compounds during maintenance. You do need to invest 3 months to learn the language, and some more to write the occasional bindings, but they will be very quickly recovered by avoiding writing CMake (not even counting asan/valgrind debugging sessions).
I can understand where they come from, especially regarding error handling: for integers whose value must not be negative, it is easier to check for underflow by checking if the result is not negative rather than by checking that the result is not eg smaller than the previous value (eg for addition).
That being said, "number must not be negative" really ought to be encoded in the type system, so that the user of the type knows that they must actually look for underflow.
I guess that part of the problem is that we don't want to check for underflow after each operation, so checking the negativity is a way to "coalesce" several checks after multiple operations. However this is fragile, because the multiple operations could end up producing a positive value, even if some intermediate values where negative.
For full safety, I don't see how we could do better than checking after each operation right now. If we're doing this, I feel like checked_add and friends from rust is a better fit than cramming an unsigned int into a signed one.
I wonder if we could design an integer type with 63 bits of value, plus one bit of "overflow/underflow poison", such that any operation that would under/overflow would saturate that bit to one, but otherwise still perform the operation on the value part. That would allow to coalesce multiple checks while keeping safety even in the presence of multiple faulty operations. I wonder how it could be implemented efficiently though
I do that all the time, even reading the source of the std, something that I cannot do sanely in C++. IME Rust code is easy to read, with symbols that are always either defined in the current file, imported, or referred to by their full path.
I would agree that forcing uninitialised variables as errors is a design mistake.