Compile time / context switching creep.
Every time I start a Rust project, it starts out nice and fast feedback loop. A few crates here & there later, and the compile time is 10+ seconds, and then off to xkcd: Compiling
Compile time / context switching creep.
Every time I start a Rust project, it starts out nice and fast feedback loop. A few crates here & there later, and the compile time is 10+ seconds, and then off to xkcd: Compiling
Cargo's semver convention slightly deviates from the original spec in that it doesn't only assume a breaking change when the major version is incremented. It always assumes breakage when the leftmost non-zero component of the version number increases (i.e., 1.4
and 1.5
are compatible, but 0.4
and 0.5
aren't).
Here is something I I think I have had trouble with. I think if you change a cargo file from using a crates.io dependency to a local dependency (or vice versa) for the same crate name (including same version number), I think cargo incremental build can become confused, assuming they are the same, based on the version number.
The solution I use is to increase the version number of the local crate, so cargo realises it is different.
A few others I don't think are in there after scanning:
IndexMut
implementationAnd I just stumbled across this bug about _
being inconsistent to boot.
Value returned from catch_unwind
can have a Drop
implementation that panics, and therefore bypass your attempt of catching all panics. Similarly Deref
, Eq
, and other traits for operator overloading can panic, so you need to be suuuuper careful in unsafe code that touches other types.
Auto traits have some gotchas: you can break downstream code by adding a private field to one of your library's types, and managing this sometimes requires careful use of PhantomData
.
With inferred lifetime bounds on structs, lifetime relationships of structs are leaky similarly to auto-traits. Choosing to declare these lifetime relationships or not has intentional semantic impact as well.
return
-position impl Trait
has similar (intentional) leakage.
Tangentially, implied vs. explicit lifetime bounds have unintentional [1] semantic impact:
trait Foo {}
trait Bar {}
impl<'a, 'b > Foo for &'a &'b () {}
impl<'a, 'b: 'a> Bar for &'a &'b () {}
fn foo<T>() where for<'a, 'b> &'a &'b T: Foo {}
fn bar<T>() where for<'a, 'b> &'a &'b T: Bar {}
fn main() {
foo::<()>(); // ok
bar::<()>(); // error: implementation of `Bar` is not general enough
}
Playground. Issue #95921 and others. There are plenty of other HRTB bugs/incompleteness too.
as far as I know; if it's intentional it's definitely still a gotcha ↩︎
One thing that I'm sure everyone uses, but people often don't think about until it causes problems -- some operators "autoref" -- so you define PartialEq
as fn eq(&self, other: &Self) -> bool
, but you just then write a==b
. Usually, if a method takes a reference, you have to explicitly write &
when you call it.
The fact that Result<T, E>
and Option<T>
implement IntoIterator<Item=T>
has definitely caused me some confusion before, especially in conjunction with for loops.
Relying on a destructor running when a borrow has expired can break your code since destructors aren't guaranteed to run. Aka "the leakpocalypse"
It's hard to get into situations where it causes huge problems without unsafe code, but it's an interesting interaction of a couple of Rust's features that can be pretty surprising
It’s always important to further explain this statement, since otherwise there’s potential for confusion.
There is no safety guarantee that destructors are run, so you (or others) can write code that skips destructors, e.g. with mem::forget
or by creating an Rc<RefCell<…>>
cycle. On the other hand, if you don’t deliberately prevent any destructor calls, or a library you’re using does so, then destructors are in fact guaranteed to run; it’s not the case that the rust compiler could simply decide to skip some destructor.
You can use destructors of local variables for safety reasons in code you control (e.g. guard objects whose destructors safely handle panicking code paths are very common in unsafe
code), and library code can rely on destructor calls for anything non-memory-safety-related, so it’s commonly considered a “logic error” to skip calling a destructor, and can have various undesirable effects (except for undefined behavior).
Speaking of gotchas that are exclusive unsafe
code there are lots more. A few that come to mind immediately:
Looking at that “leakpocalypse” link, I think that all the other thing listed on that same page would actually classify as “gotchas”, too, (and also all of them are – at least somewhat – related to unsafe
code). Also, for other readers: this “leakpocalypse” pattern is also commonly known under the term “pre-pooping your pants”.
the details of what does or doesn’t count as being “exclusive” are actually quite complicated, because other references can still exist concurrently if they are being re-borrowed ↩︎
Good news: this should get better soon
Why would a destructor run when a borrow expires? It should be run when the original owning reference goes out of scope, no?
Yes. These were types with a lifetime parameter representing a borrow of something else (struct Guard<'a>;
). They were relying on that lifetime to stay valid up until the destructor ran for the type.
However you can use std::mem::forget()
to end the borrow without running the destructor and then potentially access the data that the lifetime would have been protecting in a way that violates memory safety.
Gotcha!
Async rust have quite some gotchas, you can see what user submitted in the status quos.
https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo.html
Lifetime elision in closures and functions is different.
fn foo(a: &i32) -> &i32
means:
fn foo<'a>(a: &'a i32) -> &'a i32
but |a: &i32| -> &i32 {…}
means:
|a: &'a i32| -> &'new_lifetime_who_dis i32
and closures don't have a syntax to specify this. You can only hint the right lifetimes via type inference.