You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
It is too easy to lose #[must_use] annotations when using impl Trait, leading to footguns #51560
If I return a direct type that is must_use, I get a helpful compiler warning:
use std::iter::{self,Empty,Skip};fnx() -> Skip<Empty<i32>>{
iter::empty().skip(1)}fnmain(){x();}
warning: unused `std::iter::Skip` which must be used
--> src/main.rs:8:5
|
8 | x();
| ^^^^
|
= note: #[warn(unused_must_use)] on by default
= note: iterator adaptors are lazy and do nothing unless consumed
The equivalent (and I believe preferred) code using impl Trait gives no such useful feedback:
use std::iter;fnx() -> implIterator<Item = i32>{
iter::empty().skip(1)}fnmain(){x();}
Amusingly, this isn't a new problem, as returning a boxed trait object also has the same problem. It's arguably worse there because you've performed an allocation for no good reason, but it's also less likely because people are more reluctant to introduce unneeded allocations.
Since Rust 1.27 stabilizes #[must_use] (on functions), one possible solution would be a lint that checks the compiler-known type underneath the impl Trait and the function returning it. If the type has the annotation but the function does not, it could suggest adding the attribute to the function.
I think it would make sense for @rust-lang/lang to quickly review and agree on how we should handle this.
A lint seems plausible. Inside the same module, where we know the concrete underlying type, it would definitely make sense to pass through the #[must_use]. I'd love to know if it makes sense to automatically pass through the #[must_use] on the concrete type when used from outside the module, so that this can work automatically without needing a lint and a manual additional #[must_use] on the function.
Reacted by Jake Goulding, Wesley Wiser and Jon Gjengset
The new thing being stabilized in 1.27 is specifically #[must_use] on functions and methods (RFC 1940). #[must_use] on types, including the iterator-adapter structs like Skip, are much older.
So, I'd absolutely support adding #[must_use] for traits, which would have the same effect on a function returning -> impl Trait as #[must_use] on a type T would have for a function returning -> T.
However, I also think we ought to handle this case somehow, preferably in a way that doesn't require the user to manually propagate #[must_use] outward on every -> impl Trait function that returns a #[must_use] type.
@joshtriplett I don't think we should. The type, including its must-use'dness, is not a part of the public API in that context. If you want the function return to be treated as must-use, the solution in that case is to tag the function as must-use.
Reacted by Taylor Cramer, Eduard-Mihai Burtescu, scottmcm and Josef Brandl
@withoutboats Then in that case, would it make sense to have a lint suggesting propagation of #[must_use] from the type to the function that returns that concrete type?
@joshtriplett I don't think so. The point of returning impl Trait is to hide the return type and the facts of its API, the fact that the type is must-use seems no different from other facts about the return type.
Reacted by Taylor Cramer, Eduard-Mihai Burtescu and Josef Brandl
As alluded to above, I argue that this is a bug in the must-use lint due to its antedating impl-Trait; I don't see sufficient cause here to complicate the language with another lint or feature. A pull request is forthcoming.
So let me summarize to see if I understand correctly what you're planning for must_use for traits:
#[must_use]structFoo{}#[must_use]traitMustUseTrait{}implMustUseTraitforFoo{}traitCanUseTrait{}implCanUseTraitforFoo{}fnget_foo() -> Foo{Foo{}}fnget_must_use_trait() -> implMustUseTrait{Foo{}}fnget_can_use_trait() -> implCanUseTrait{Foo{}}fnmain(){get_foo();// Warning (because of must_use on Foo and MustUseTrait)get_must_use_trait();// Warning (because of must_use on MustUseTrait)get_can_use_trait();// No warning (must_use on Foo does not matter because the type is anonymous)}
I think expanding the applicability of must_use should probably be introduced using an RFC. It seems to me that there are two different approaches proposed and an RFC could clarify why we chose a certain approach:
@zackmdavis proposes to evaluate the lint using the concrete type and provided a PR to this effect.
This is incompatible with @withoutboats statement with which I agree with: "The type, including its must-use'dness, is not a part of the public API in that context [i.e. return impl Trait]". I also agree with @cramertj that must_use for traits is something we should investigate and pursue. This would be extremely helpful for futures because forgetting to poll needs to be obvious.
@withoutboats Then in that case, would it make sense to have a lint suggesting propagation of #[must_use] from the type to the function that returns that concrete type?
@joshtriplett I don't think so. The point of returning impl Trait is to hide the return type and the facts of its API, the fact that the type is must-use seems no different from other facts about the return type.
Can you provide an example of a case where it is the correct thing to return a concrete type that is tagged as #[must_use] through impl Trait and not use the result of the function?
My original premise is that this is an unintentional mistake in 95-99% of the cases, which feels like the ideal case for a lint. In the (rare) case where the author of the impl Trait-returning function deliberately wants to allow forgetting to use the return value, they can allow the lint.
I don't know exactly which "other facts" you mean, but I can think of two primary things:
"sub" traits, which are not automagically propagated:
I think that the must_use case is different from the "sub trait" case because you will rather quickly run into "well, my code doesn't compile" errors when you forget to return the "sub trait". You then have to decide if you meant to expose that interface.
Ergonomics: Trait objects already have the issue of explicitly needing to declare Send/Sync-ability, and not extending this problem to abstract return types is desirable. In practice, most uses of this feature would have to add explicit bounds for OIBITS if they wanted to be maximally usable.
I think there's a case to be made that #[must_use] is already an ergonomics-based feature as it helps prevent the user from writing code they didn't mean to.
As a concrete side note, I opened this because I did it myself: I wrote some code that returned impl Future<...> and was confused for a minute because the compiler didn't have any warnings. I'm used to those warnings and they are part of my development flow. Having them "suddenly go missing" just because I wanted to use impl Trait felt like trying to step on the last stair that isn't there.
For me, it's right up there with #36375 in that my "normal" flow of programming that I've established since ~Rust 0.12 doesn't quite work the same with impl Trait.
@shepmaster IMO the more conservative approach would be to not leak must_use on the concrete type. If that turns out to be the wrong call, it can always be added later. I think defining must_use on traits (e.g. on Future and Iterator) will already be enough to catch all the bugs we're trying to prevent.
The big advantage of defining must_use on the trait is that this system can also work for unnameable types like the return type of an async function. Additionally it also avoids all the boilerplate code that defines each concrete iterator or future type as much_use.
@shepmaster I think the current approach of "stick #[must_use] on the dozens of types implementing Iterator and Future" just doesn't scale and this is an example where it goes wrong.
Also consider type parameters or associated types that are known implement those traits.
IMO they should get the same treatment by #[must_use], without knowing the concrete type.
To be clear, I'm totally happy with implementing #[must_use] for traits, but that particular option has been available "forever" but doesn't seem to be implemented for whichever reason. On the flip side, @zackmdavis was able to get something in the ballpark of a lint fairly quickly.
I really just don't want to see this particular feature held up by a months (years?) long discussion of the ideal Platonic implementation of #[must_use] for traits.
the more conservative approach would be to not leak must_use on the concrete type
Isn't that what the current implementation does? Am I missing something?
it can always be added later.
I believe that's what I'm proposing... 😕
this system can also work for unnameable types like the return type of an async function
avoids all the boilerplate code
the current approach [...] just doesn't scale
consider type parameters or associated types
Yes, I 💯 believe that that's a better option, and I'll add one more: presumably it will also work for trait objects. I just don't want to lose what existing ergonomics we have until someone implements #[must_use] for traits.
These don't even have to be mutually exclusive options! The lint can catch cases that a #[must_use] trait wouldn't — such as when a given instance of a trait should be must use but not the trait itself.
The lint could even be removed once the better solution is created, if we felt it didn't carry its weight anymore.
I also want to suggest a more aggressive approach: existential impl Trait is always #[must_use] no matter of its underlying type. If a function is returning an impl Trait thing, you were probably meant to use it in some way.
@shepmaster Thanks for the compelling argument! I agree with what I think is the implication of your post: this question boils down to "Is #[must_use] more like an auto trait, or a regular API fact?" I'm not sure of the answer.
@oli-obk What is the distinction with impl Trait vs any other return type? Isn't it plausible that something that returns impl Trait also has a useful side effect, and the return is not relevant all the time?
Activity
shepmaster commentedon Jun 14, 2018
Since Rust 1.27 stabilizes
#[must_use](on functions), one possible solution would be a lint that checks the compiler-known type underneath theimpl Traitand the function returning it. If the type has the annotation but the function does not, it could suggest adding the attribute to the function.joshtriplett commentedon Jun 14, 2018
I think it would make sense for @rust-lang/lang to quickly review and agree on how we should handle this.
A lint seems plausible. Inside the same module, where we know the concrete underlying type, it would definitely make sense to pass through the
#[must_use]. I'd love to know if it makes sense to automatically pass through the#[must_use]on the concrete type when used from outside the module, so that this can work automatically without needing a lint and a manual additional#[must_use]on the function.eddyb commentedon Jun 14, 2018
Why is the
#[must_use]not on, say,Iterator?cramertj commentedon Jun 14, 2018
I agree with @eddyb that
#[must_use]traits seem like the better solution here than trying to "leak" must-used-ness.oli-obk commentedon Jun 14, 2018
In that case, is it fine that returning an
impl Debugof real typeResultwill drop the must-used-ness?Maybe we should suggest to the author of the function to add the attribute to the function if the real type is
#[must_use]eddyb commentedon Jun 14, 2018
@oli-obk Right, there's nothing meaningfully tying such a function to
Result, ifResultis hidden.zackmdavis commentedon Jun 14, 2018
The new thing being stabilized in 1.27 is specifically
#[must_use]on functions and methods (RFC 1940).#[must_use]on types, including the iterator-adapter structs likeSkip, are much older.zackmdavis commentedon Jun 14, 2018
We may need to add an arm for
TyAnonin this match?rust/src/librustc_lint/unused.rs
Lines 61 to 72 in 5205ae8
joshtriplett commentedon Jun 14, 2018
So, I'd absolutely support adding
#[must_use]for traits, which would have the same effect on a function returning-> impl Traitas#[must_use]on a type T would have for a function returning-> T.However, I also think we ought to handle this case somehow, preferably in a way that doesn't require the user to manually propagate
#[must_use]outward on every-> impl Traitfunction that returns a#[must_use]type.withoutboats commentedon Jun 14, 2018
@joshtriplett I don't think we should. The type, including its must-use'dness, is not a part of the public API in that context. If you want the function return to be treated as must-use, the solution in that case is to tag the function as must-use.
joshtriplett commentedon Jun 14, 2018
@withoutboats Then in that case, would it make sense to have a lint suggesting propagation of
#[must_use]from the type to the function that returns that concrete type?withoutboats commentedon Jun 14, 2018
@joshtriplett I don't think so. The point of returning impl Trait is to hide the return type and the facts of its API, the fact that the type is must-use seems no different from other facts about the return type.
zackmdavis commentedon Jun 15, 2018
As alluded to above, I argue that this is a bug in the must-use lint due to its antedating impl-Trait; I don't see sufficient cause here to complicate the language with another lint or feature. A pull request is forthcoming.
1 remaining item
zackmdavis commentedon Jun 15, 2018
Hm, perhaps this was too confidently worded: see description of the new pull request #51571.
MajorBreakfast commentedon Jun 15, 2018
So let me summarize to see if I understand correctly what you're planning for must_use for traits:
MajorBreakfast commentedon Jun 15, 2018
I think expanding the applicability of must_use should probably be introduced using an RFC. It seems to me that there are two different approaches proposed and an RFC could clarify why we chose a certain approach:
shepmaster commentedon Jun 15, 2018
Can you provide an example of a case where it is the correct thing to return a concrete type that is tagged as
#[must_use]throughimpl Traitand not use the result of the function?My original premise is that this is an unintentional mistake in 95-99% of the cases, which feels like the ideal case for a lint. In the (rare) case where the author of the
impl Trait-returning function deliberately wants to allow forgetting to use the return value, they can allow the lint.I don't know exactly which "other facts" you mean, but I can think of two primary things:
"sub" traits, which are not automagically propagated:
Auto traits, which are automagically propagated:
I think that the
must_usecase is different from the "sub trait" case because you will rather quickly run into "well, my code doesn't compile" errors when you forget to return the "sub trait". You then have to decide if you meant to expose that interface.I know the auto trait aspect was much discussed, so I don't wish to re-litigate it. My point here is that we made the decision to automatically pass them for ergonomics:
I think there's a case to be made that
#[must_use]is already an ergonomics-based feature as it helps prevent the user from writing code they didn't mean to.shepmaster commentedon Jun 15, 2018
As a concrete side note, I opened this because I did it myself: I wrote some code that returned
impl Future<...>and was confused for a minute because the compiler didn't have any warnings. I'm used to those warnings and they are part of my development flow. Having them "suddenly go missing" just because I wanted to useimpl Traitfelt like trying to step on the last stair that isn't there.For me, it's right up there with #36375 in that my "normal" flow of programming that I've established since ~Rust 0.12 doesn't quite work the same with
impl Trait.MajorBreakfast commentedon Jun 15, 2018
@shepmaster IMO the more conservative approach would be to not leak must_use on the concrete type. If that turns out to be the wrong call, it can always be added later. I think defining must_use on traits (e.g. on
FutureandIterator) will already be enough to catch all the bugs we're trying to prevent.The big advantage of defining must_use on the trait is that this system can also work for unnameable types like the return type of an async function. Additionally it also avoids all the boilerplate code that defines each concrete iterator or future type as much_use.
eddyb commentedon Jun 15, 2018
@shepmaster I think the current approach of "stick
#[must_use]on the dozens of types implementingIteratorandFuture" just doesn't scale and this is an example where it goes wrong.Also consider type parameters or associated types that are known implement those traits.
IMO they should get the same treatment by
#[must_use], without knowing the concrete type.shepmaster commentedon Jun 15, 2018
To be clear, I'm totally happy with implementing
#[must_use]for traits, but that particular option has been available "forever" but doesn't seem to be implemented for whichever reason. On the flip side, @zackmdavis was able to get something in the ballpark of a lint fairly quickly.I really just don't want to see this particular feature held up by a months (years?) long discussion of the ideal Platonic implementation of
#[must_use]for traits.Isn't that what the current implementation does? Am I missing something?
I believe that's what I'm proposing... 😕
Yes, I 💯 believe that that's a better option, and I'll add one more: presumably it will also work for trait objects. I just don't want to lose what existing ergonomics we have until someone implements
#[must_use]for traits.These don't even have to be mutually exclusive options! The lint can catch cases that a
#[must_use]trait wouldn't — such as when a given instance of a trait should be must use but not the trait itself.The lint could even be removed once the better solution is created, if we felt it didn't carry its weight anymore.
oli-obk commentedon Jun 15, 2018
I also want to suggest a more aggressive approach: existential
impl Traitis always#[must_use]no matter of its underlying type. If a function is returning animpl Traitthing, you were probably meant to use it in some way.shepmaster commentedon Jun 15, 2018
😮 I wonder how hard it would be to turn such a thing on to see the impact. 😨
withoutboats commentedon Jun 15, 2018
@shepmaster Thanks for the compelling argument! I agree with what I think is the implication of your post: this question boils down to "Is
#[must_use]more like an auto trait, or a regular API fact?" I'm not sure of the answer.@oli-obk What is the distinction with
impl Traitvs any other return type? Isn't it plausible that something that returnsimpl Traitalso has a useful side effect, and the return is not relevant all the time?