Note: let _ = conn
does not move/drop conn
. If you want to drop it, you need to write drop(conn)
. This also makes #[ignore_must_move]
unnecessary, since calling drop(conn)
moves conn
into the function call.
Additionally, a majority (though not all) of types which would want to be #[must_use]
are so because they are Write
-like. But for these,
The difficulty with any sort of linear typing/hinting is that it disappears when given to a generic interface (e.g. drop
again). That's not to say a warning wouldn't be useful (#[must_use]
is a lint and very effective). But it would be somewhat limited by that and easy to "defeat."
I suppose there's an alternative: warn if passing a #[must_use]
type to a generic parameter which isn't itself annotated with #[must_move]
(thus doesn't not get unmoved_must_move
warnings). But personally I'd still mark fn std::mem::drop
as taking #[must_move]
, since an explicit drop is specifically requesting the nullary (no further arguments) final use.
While it's possible to come up with a reasonable semantic for generic functions that maintains the lint, the real problem is generic types. What about Box<Connection>
, or Arc<Connection>
, or Mutex<Connection>
, etc.? This is a definite case where #[must_move]
will end up covered unless there's some way to inherit "must_movedness." It's not sufficient to always inherit because of borrowing types (e.g. MutexGuard<'a, Connection>
), and it's not singly sufficient either to infer based on whether a field owns that type by value, since dropping types may involve dropping types they don't directly contain, e.g. containers based off of raw pointers.
Then there's also many-collections to consider; dropping Vec<T>
drops zero-or-more T
. You'd think .into_iter()
ing it would be sufficient, but dropping vec::IntoIter<T>
also drops zero-or-more T
. If you want to avoid false positives, but still lint if the Vec
could nullary-drop some #[must_move]
objects, you'll need some way to cover the #[must_move]
lint when the types are consumed by Iterator::for_each
or a for
loop... except not the for
loop if it potentially break
s iteration before exhausting the iterator.
Ultimately, doing linear types as a lint/warning has all of the same issues as doing it as a trait/error. The only difference is that it can't be relied upon, and that imprecision is less immediate of a problem since it's "just a lint," so missing or spurious restrictions are "only" unfortunate footgun potential rather than build-breaking errors. But spurious warnings are still highly problematic, since they directly contribute to warning fatigue and just turning off the lint entirely.
But, to be completely fair, #[must_use]
is a very simple lint which is "defeatable" by all of these things, and is still a useful lint. #[must_move]
would still be useful even if it only works in extremely simple cases. But trying to go further than that (i.e. by making generics sometimes cover #[must_move]
responsibility and sometimes not) only leads to issues coming from expecting the lint to be more powerful than it is (or potentially even can be).
What's interesting is that Rust actually does have a very minimal, extremely limited support for linear types: in a const
context, you can take a generic type parameter by value, but you are not allowed to drop (anything which (potentially) has drop glue and captures) it (because the drop glue is not necessarily valid in a const
context). There it's important that the check is conservative and overestimates types which could potentially run nonconst drop glue, whereas the lint would prefer to be conservative in the other direction and avoid any false positives, such that the lint is actually useful when it fires.
But it's worth noting that even the const
linearity ignores drops as a result of panicking/unwinding. Every proposal for linearity in Rust (even those around async drop/cancellation) still provides that a nullary (and synchronous/blocking) drop must be possible/available in the case of an unwind. But what makes linear types (or "higher RAII") truly powerful is the ability to provide extra arguments to cleanup which are required in order to destroy the value, which is unfortunately just fundamentally incompatible with unwinding.
Rust tries very hard to keep all warnings/errors pre-monomorphization, based solely on the generic signature and independent of what types the generics are instantiated with. Most monomorphization isn't even done during cargo check
, and only performed once required by cargo build
in order to lower to the codegen backend. (The exception is when things are used by a top-level (nongeneric) const
context, in which case they're monomorphized in order to evaluate them.)