Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impl Trait + 'static is not 'static if returned from a generic function #76882

Closed
WaffleLapkin opened this issue Sep 18, 2020 · 7 comments
Closed
Labels
A-impl-trait Area: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch. C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@WaffleLapkin
Copy link
Member

I tried this code:

trait Trait {}
impl Trait for () {}

fn get_trait<I>() -> impl Trait + 'static {
    ()
}

fn assert_static<A: 'static>(_: A) {}

fn test<T>(_: T) {
    assert_static(get_trait::<T>());
}

(playground)

I expected that rustc will compile this code. But it doesn't. Instead, I've got this error:

error[E0310]: the parameter type `T` may not live long enough
  --> src/lib.rs:11:5
   |
10 | fn test<T>(_: T) {
   |         - help: consider adding an explicit lifetime bound...: `T: 'static`
11 |     assert_static(get_trait::<T>());
   |     ^^^^^^^^^^^^^ ...so that the type `impl Trait` will meet its required lifetime bounds

The error seems weird because the return type of get_trait is explicitly marked as 'static and T/I is not used anywhere at all.

This may be related to #49431 though I'm not entirely sure.

Meta

Checked on 1.46.0 stable and 1.48.0-nightly (2020-09-17 f3c923a)

@WaffleLapkin WaffleLapkin added the C-bug Category: This is a bug. label Sep 18, 2020
@jonas-schievink jonas-schievink added the A-impl-trait Area: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch. label Sep 18, 2020
@jyn514 jyn514 added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Sep 18, 2020
@est31
Copy link
Member

est31 commented Sep 19, 2020

You need to add a :'static requirement to the generic T. This works:

trait Trait {}
impl Trait for () {}

fn get_trait<I: 'static>() -> impl Trait + 'static {
    ()
}

fn assert_static<A: 'static>(_: A) {}

fn test<T: 'static>(_: T) {
    assert_static(get_trait::<T>());
}

The core reason for this behaviour is the variance behaviour of impl Trait in return position: the returned impl Trait is always variant over all generic input parameters, even if not technically used. This is done so that if you change the internal implementation of a public API returning impl Trait you don't have to worry about introducing an additional variance param to the API. This could break downstream code and is thus not desireable for Rust's semver system.

@WaffleLapkin
Copy link
Member Author

That makes some sense, but in my opinion, this should be only 'behaviour by default'. If the lifetime is explicitly set (+ 'a/+ 'static) then the programmer that has set it is responsible to set it right and do not break API in the future.

Also, note that with -> impl Trait + 'static you can't return non static generic params, so there is no way to use them without changing signature (and so the limitation discussed doesn't really help with semver):

fn erase<I: Trait>(x: I) -> impl Trait + 'static {
    x
}
error[E0310]: the parameter type `I` may not live long enough
 --> src/lib.rs:4:29
  |
4 | fn erase<I: Trait>(x: I) -> impl Trait + 'static {
  |          --                 ^^^^^^^^^^^^^^^^^^^^ ...so that the type `I` will meet its required lifetime bounds
  |          |
  |          help: consider adding an explicit lifetime bound...: `I: 'static +`

(playground)

@WaffleLapkin
Copy link
Member Author

Note that this behaviour is also inconsistent with lifetimes which are not captured by default:

trait Trait {}
impl Trait for &() {}

fn get_trait<I>(a: &()) -> impl Trait {
    a
}
error[E0759]: `a` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
 --> src/lib.rs:5:5
  |
4 | fn get_trait<I>(a: &()) -> impl Trait {
  |                    --- this data with an anonymous lifetime `'_`...
5 |     a
  |     ^ ...is captured here...
  |
note: ...and is required to live as long as `'static` here
 --> src/lib.rs:4:28
  |
4 | fn get_trait<I>(a: &()) -> impl Trait {
  |                            ^^^^^^^^^^
help: to declare that the `impl Trait` captures data from argument `a`, you can add an explicit `'_` lifetime bound
  |
4 | fn get_trait<I>(a: &()) -> impl Trait + '_ {
  |                                       ^^^^

(playground)

@ais523
Copy link

ais523 commented Mar 27, 2023

It seems that the current behaviour of capturing lifetimes from generic arguments as lifetimes in the impl Trait has been only partially implemented, and produces some strange consequences in practice. (I noticed the issue while answering this StackOverflow question, which was wondering why the return value of this sort of function couldn't be returned as an impl Trait + 'static but could be coerced to a dyn Trait + 'static inside a Box.)

Here's an example of some very strange behaviour caused by this sort of capture:

#![feature(type_alias_impl_trait)]
#![feature(unsize)]

use core::marker::Unsize;
use core::fmt::Debug;

pub fn returns_unrelated_impl<T>() -> impl Debug + 'static { () }

// doesn't compile: the compiler thinks the `impl Debug` should capture `'a`
pub fn test_1<'a>() -> impl Debug {
    returns_unrelated_impl::<&'a ()>()
}

// doesn't compile: the compiler thinks `ImplUnsizeStatic` should capture `'a`
pub fn test_2a<'a>() -> impl Debug {
    type ImplUnsizeStatic = impl Unsize<dyn Debug> + 'static;

    let rv1: ImplUnsizeStatic = returns_unrelated_impl::<&'a ()>();
    let rv2: Box<dyn Debug> = Box::new(rv1);
    rv2
}

// does compile, because we aren't assigning an `impl` to another `impl`
pub fn test_2b<'a>() -> impl Debug {
    fn verify_impl_unsize_static<T: Unsize<dyn Debug> + 'static>(x: &T) {}

    let rv1 = returns_unrelated_impl::<&'a ()>();
    verify_impl_unsize_static(&rv1);
    let rv2: Box<dyn Debug> = Box::new(rv1);
    rv2
}

(playground)

The check that the return value of returns_unrelated_impl captures the generic argument T seems only to be made when the impl Debug it returns is coerced to another impl Trait type. test_2a shows that the return value of returns_unrelated_impl is not usable as an impl Unsize<dyn Debug> + 'static. Meanwhile, test_2b shows that the return value does in fact implement Unsize<dyn Debug> + 'static (a fact that makes it possible to box the return value as a Box<dyn Debug> + 'static and then return it as an impl Debug). It is very astonishing behaviour for these two cases to be considered to be different by the compiler.

This makes me concerned that if "an impl Trait return type captures the generic type arguments of the function it's returned from" is intended behaviour, that restriction has not actually been implemented correctly, and thus return-position impl Trait has been accidentally stabilised in a state that permits more code than it's supposed to. If the capturing of generic arguments were required for soundness, this would likely be a soundness hole: however, it doesn't obviously appear to be required for soundness, so there may be simpler fixes (e.g. to not capture the arguments at all, which is what most programmers including me seem to expect will happen).

@Ddystopia
Copy link
Contributor

Ddystopia commented Sep 11, 2023

Another example, when that behavior is inappropriate

#![feature(return_position_impl_trait_in_trait)]

trait Foo: 'static { }

trait Trait {
    fn foo(&mut self) -> impl Foo + 'static;

    fn bar(&mut self, foo: impl Foo + 'static) {
        std::hint::black_box((self, foo));
    }
}

struct FooImpl;
impl Foo for FooImpl { }

struct TraitImpl;
impl Trait for TraitImpl {
    fn foo(&mut self) -> impl Foo + 'static {
        FooImpl
    }
}

fn main() {
    let mut t = TraitImpl;
    let foo = t.foo();
    t.bar(foo);
}

(playground)

(here Foo: 'static so other bound are not needed, but I've included both for completeness)

Here default behavior stands in the way of me creating an API, that would be possible without rpitit - you could change return type of Trait::foo to FooImpl or use associated type Trait::TraitsFoo and it will compile. But now I can't do it, because it captures mutable lifetime, even if it is clear that impl Foo is 'static, so it is not either & or &mut.

@WaffleLapkin WaffleLapkin changed the title impl Trait + 'static is not static if returned from generic function impl Trait + 'static is not 'static if returned from a generic function Sep 15, 2023
@compiler-errors
Copy link
Member

Fixed by #95474.

@WaffleLapkin
Copy link
Member Author

nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-impl-trait Area: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch. C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

7 participants