Open geo-ant opened 1 year ago
So are you saying people should stop using C++ and all future code should be in Rust?
Kudos to you for such a beautifully written article!
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.
Kudos to you for such a beautifully written article!
Thank you kindly @ToiletCommander
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.
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 callingerase
. 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.
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.)
@npalladium thanks so much, I'm very happy with how that article turned out and I'm really happy about the positive feedback!
Very informative article. Thanks!
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 tovoid
.
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 callingerase
.
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.
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 whenmap
anddefault_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
fordefault_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 tovoid
.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 usestd::convert::Infallible
(or any empty enum) instead.Since a Rust
Mutex
protects data, I'd argue that your example is closer tovoid
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 :)
Comments for this article go here and are displayed on the page instantly.