HNHacker News
TopNewBestAskShowJobs

dureuill

456 karma · joined July 24, 2021

submissionscomments
dureuill··on Current hardware trends make C++ exceptions harder to justify
That's probably correct, I did not use concepts at the moment (stuck in C++14 right now). I certainly have my beefs with both features (modules that are orthogonal to namespace, no standard way to find modules, new and exciting ways of committing ODR violations, generally a complicated module system with quirks when there is so much prior art on modules in other languages, concepts being structural and not nominal, concepts being a "lower bound" on behavior and not an "upper bound" (thus not eliminating duck typing)), but my larger point is the scope of these changes to the language, not their (purported) benefit that I'm by and large unable to assess right now.
dureuill··on Current hardware trends make C++ exceptions harder to justify
> There are times when writing/profiling Rust when I wish I had access to exceptions instead of `Result` propagation.

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?

dureuill··on Current hardware trends make C++ exceptions harder to justify
I'm not convinced they're comparable.

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.

dureuill··on Data races in Python, despite the Global Interpreter Lock
This is not a data race in the sense that Rust prevents.

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...

dureuill··on C meeting is over. C23 added:
My experience is mainly with C++, but from my own evaluation I'm x2 to x3 more productive overall in Rust than in C++. The reasons are a simpler build system that makes using the ecosystem faster, more expressive type system (iterators with combinators, sum types), the safety part that allows to not accidentally introduce UB and to not waste time with static and runtime analysis, shortening debug and review time. This compounds over time to give a better maintenance time.

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.

dureuill··on C meeting is over. C23 added:
> the maturity of the libraries in that area,

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)

dureuill··on C meeting is over. C23 added:
You could get productive in Rust in 3 months max, and then the productivity gains would quickly outweigh the initial investment
dureuill··on C meeting is over. C23 added:
In general, I think they should go for Rust. See [1].

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.

[1]: http://cliffle.com/blog/prefer-rust/

dureuill··on The simplest guide to error handling in Rust
Simple indeed.

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.

dureuill··on Some mistakes Rust doesn't catch
Yes, which also serves as a way to explicitly point out that the "surprising operation" is intended. Hopefully this even reminds you of putting an explanatory comment there.
dureuill··on Some mistakes Rust doesn't catch
> The thing is, C also makes me aware of what is happening, by letting programs fail when I forget about certain things.

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.

dureuill··on Swift and C++ interoperability workgroup announcement
Yes, cxx.rs is really good for what it is! There's also autocxx that aims at generating cxx bridges directly. What Rust lacks is, to my knowledge, a C++ interoperability working group. cxx.rs (which is useable but by no means finished) could be a topic of such a working group. Direct compiler support of C++ could be another.

A better C++ rust interop story is a critical piece for Rust's success IMO

dureuill··on Swift and C++ interoperability workgroup announcement
Great, now I want the same for Rust <-> C++
dureuill··on C++ Frequently Questioned Answers (2009)
Sorry for not being clear. Should have written "when I start writing Rust" at this point. I have been using Rust personally since 2016, and professionally since 2019. Most of our codebase is naturally still in C++ (we're not aiming at "rewriting the world"), so I can frequently feel the difference between the two languages, which is why my preference is so clear-cut.
dureuill··on C++ Frequently Questioned Answers (2009)
I measured it. On multiple projects. Some I started writing in Rust, so this is not the "rewrite effect" at play. I observe x2 to x3 on development time alone, long term if we compound review, debugging and maintenance I wouldn't be surprised to see if it tends to an order of magnitude of difference.

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.

dureuill··on C++ Frequently Questioned Answers (2009)
> I don't think the OP meant iterating on performance problems specifically, just generally iterating on code as one does when they write it. Also, every programmer at some point iterates about performance problems, not matter their experience, or language (or implementation), or runtime that's being used.

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).

dureuill··on C++ Frequently Questioned Answers (2009)
> But as the market exists right now, Rust developers are twice as expensive and the product they deliver is twice as bad.

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.

dureuill··on Rust in 2022
there is lib.rs that I find much more efficient to discover crates.

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.

[0]: https://lib.rs/web-programming/http-client

dureuill··on “Modern” C++ Lamentations (2018)
Thanks. We've been using external projects since some of them are old style CMake, ripe with global variables, but apparently we could've used add_subdirectory for some other libs. I'll keep that in mind for future refactorings and libraries.
dureuill··on “Modern” C++ Lamentations (2018)
I never found a C++ IDE capable of understanding our root CMake project so it can gives us autocompletion and inline errors.

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.

dureuill··on “Modern” C++ Lamentations (2018)
Curious what kind of code you have. Sure, we don't have anywhere close to our C++ codebase in Rust, but we did integrate with a library that's on the bigger side, and compile times of the Rust part have been negligible compared to the C++ parts.
dureuill··on Rust should have larger stdlib
The difference between the two is that "OnceCell" is more of a "vocabulary type" that is used to build other types and might get exported in public APIs, while regex is more of a "leaf type", if that makes sense. There's also something fundamental in "how do I build a static variable?".
dureuill··on Folly: An open-source C++ library developed and used at Facebook
I wouldn't start a green field C++ project if I can help it[1], but if I had to I would probably go with the standard library with maybe as little boost as possible.

[1]: http://cliffle.com/blog/prefer-rust/

dureuill··on Folly: An open-source C++ library developed and used at Facebook
Funny, I have a full time C++ job, and really don't see the appeal. Rust feels superior in every regards and especially dev experience. Want to exchange jobs ;-)?
dureuill··on Rewriting Libimagequant in Rust for Portability
A subset of C++ cannot simultaneously be secure and useful, because to be secure a subset would need to ban pointers, references, iterators, moves and the ability to include code that is not part of this subset (for the last point modules might help, but they aren't here and they introduce their own ODR violations cases, so "safe" is a stretch even for them).

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).

dureuill··on Almost Always Unsigned
I guess what the parent is saying is that, while the same kind of bugs can happen with signed integers, they are less common than with unsigned integers because they typically involve very big or very small values such as INT_MAX or INT_MIN. This is due to the fact that the "most common values" (close to 0) are at the beginning of the range of unsigned integers, but at the middle of the range of signed integers, so the risk of underflow is lower for signed integers in practice.

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

dureuill··on A Review of the Zig Programming Language (Using Advent of Code 2021)
> Try and read a moderately complex Rust crate and it can be mind boggling to work out what is going on.

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.

dureuill··on Zig 0.9.0
Yes, users can misuse warnings by ignoring them in CI, but the ergonomic cost of forcing unused variables as errors is disproportionate in regard to this. This affects all users regardless of if they would deny warnings in CI, and leads to either removing/adding some code repeatedly so it compiles at all, or worse adding fake uses, defeating the purpose of the warning. I am using C++ with -Werror during development because it is the only way to keep my sanity (functions not returning a value in C++ is a warning), but it is an ergonomic disaster that I am happy to avoid when using Rust (where the right place to deny warnings is in CI).

I would agree that forcing uninitialised variables as errors is a design mistake.

dureuill··on Zig 0.9.0
agreed. Rust has a warning for unused items, which is indeed interesting to catch logic errors (variables that should be used, but aren't). No need to fail compilation for this. It is then on you to deny warnings in CI to ensure that they are all handled before the code actually lands
dureuill··on Rust support in the Linux kernel
Thank you!
← PreviousPage 6 of 7Next →