geo-ant / blog

My public notepad in blog form
https://geo-ant.github.io/blog
MIT License
1 stars 0 forks source link

Comments: Rust vs Common C++ Bugs #41

Open geo-ant opened 1 year ago

geo-ant commented 1 year ago

Comments for this article go here and are displayed on the page instantly.

AdriaanPrinsloo commented 1 year ago

So are you saying people should stop using C++ and all future code should be in Rust?

realquantumcookie commented 1 year ago

Kudos to you for such a beautifully written article!

geo-ant commented 1 year ago

So are you saying people should stop using C++ and all future code should be in Rust?

@AdriaanPrinsloo I would not go so far, but clearly there is a need for many of the features that Rust brings to the table. Evidenced e.g. by projects like Cpp2/CppFront or Carbon. Both are pushed forward by ISO C++ committee members.

geo-ant commented 1 year ago

Kudos to you for such a beautifully written article!

Thank you kindly @ToiletCommander

aleksanderkrauze commented 1 year ago

Very nice article!

I would add also another class of bugs that are prevented in Rust by borrow checker, and that is iterator invalidation.

std::vector<int> vec = {1, 2, 3, 4, 5, 6};
for (auto it = vec.begin(), it < vec.end(); ++it) {
    if (*it % 2 == 0)
        vec.erase(it);
}

The above snippet will cause UB, because vec.end() will become invalidated by calling erase. This (and many other similar problems) arise from taking both immutable and mutable reference to data at the same time, which is disallowed and statically checked by the borrow checker in Rust.

geo-ant commented 1 year ago

Very nice article!

I would add also another class of bugs that are prevented in Rust by borrow checker, and that is iterator invalidation.

std::vector<int> vec = {1, 2, 3, 4, 5, 6};
for (auto it = vec.begin(), it < vec.end(); ++it) {
    if (*it % 2 == 0)
        vec.erase(it);
}

The above snippet will cause UB, because vec.end() will become invalidated by calling erase. This (and many other similar problems) arise from taking both immutable and mutable reference to data at the same time, which is disallowed and statically checked by the borrow checker in Rust.

Hey @aleksanderkrauze, thanks for the kind words. Yes, there are tons of other bugs that are definitely worth looking at. I just tried to keep the scope of the article limited, which is why I did not stray from the bugs that Louis presented in his conference talk.

npalladium commented 1 year ago

This is an amazing article!

(I have nothing top add to the discussion but just want to let you know that I loved the material and the writing.)

geo-ant commented 1 year ago

@npalladium thanks so much, I'm very happy with how that article turned out and I'm really happy about the positive feedback!

memark commented 1 year ago

Very informative article. Thanks!

korrat commented 1 year ago

Thank you for the interesting and insightful read. I have a few comments on some aspects of the post though.

In your get_or_default example, I'm curious whether you are aware that your lifetimes are unnecessarily complex? You can achieve the same result with

fn get_or_default<'a>(
  map : &'a BTreeMap<String,String>,
  key : &String,
  default_val : &'a String) -> &'a String 
{
    match map.get(key) {
        Some(val) => val,
        None => default_val,
    }
}

This works even when map and default_val have different lifetimes. Rust automatically infers 'a to be the shorter of the two lifetimes.

Furthermore, it's usually more idiomatic to return string slices (&str) instead of borrowed strings (&String). This would also allow you to use &str for default_val, removing the need for allocations to use your API.

My second point is something of a pet peeve of mine, so apologies in advance. It's concerned with the following statement in your Mutex<()> example:

() in Rust is (in this case) equivalent to void.

In C, void serves two roles, which Rust separates due to its more expressive type system. The first role, as a return type specification (void foo(…)), is indeed similar to () in Rust, in that it applies to function that do not return a value.

However, void's second role is as a type, in which it is an incomplete type that cannot be constructed. In Rust, the “never” type (!) fills this role. Since ! is unstable at the moment, you can also use std::convert::Infallible (or any empty enum) instead.

Since a Rust Mutex protects data, I'd argue that your example is closer to void as a type. In this case, a closer match would be an empty struct in C.

My third and final point is in response to @aleksanderkrauze's iterator invalidation example, specifically:

The above snippet will cause UB, because vec.end() will become invalidated by calling erase.

While vec.erase invalidates iterators after (and including) it, vec.end() is not stored but computed anew for every iteration, so I think invalidation of vec.end() is not the problem here. I think it would be invalidated though, which would be UB.

geo-ant commented 1 year ago

Thank you for the interesting and insightful read. I have a few comments on some aspects of the post though.

In your get_or_default example, I'm curious whether you are aware that your lifetimes are unnecessarily complex? You can achieve the same result with [...] This works even when map and default_val have different lifetimes. Rust automatically infers 'a to be the shorter of the two lifetimes.

Thanks @korrat, I did not know that. I was afraid this was going to be one of those cases where the lifetimes would be coupled in such a way that the temporary was going to have to live as long as the whole map. I'll look into that but leave the example unchanged for now. Glad you left this comment here for reference!

Furthermore, it's usually more idiomatic to return string slices (&str) instead of borrowed strings (&String). This would also allow you to use &str for default_val, removing the need for allocations to use your API.

Yes, the whole function is not idiomatic and I do acknowledge that in the text. I did not want to complicate things for C++ developers reading this, because translating std::string const& s to s: &String seemed (to me) to be less confusing than introducing string slices as well.

My second point is something of a pet peeve of mine, so apologies in advance. It's concerned with the following statement in your Mutex<()> example:

() in Rust is (in this case) equivalent to void.

In C, void serves two roles, which Rust separates due to its more expressive type system. The first role, as a return type specification (void foo(…)), is indeed similar to () in Rust, in that it applies to function that do not return a value.

However, void's second role is as a type, in which it is an incomplete type that cannot be constructed. In Rust, the “never” type (!) fills this role. Since ! is unstable at the moment, you can also use std::convert::Infallible (or any empty enum) instead.

Since a Rust Mutex protects data, I'd argue that your example is closer to void as a type. In this case, a closer match would be an empty struct in C.

I'm very sympathetic to pet peeves. Got a few of my own :D . I'm aware that the comparison is a bit flawed but I hope it does not take away from the general message. Thanks for clarifying this for anyone that takes my equivalence too seriously :)