简体   繁体   中英

Generic types that depend on another generic in Rust

I'm trying to create a struct that is generic, with a bound that the generic implement a trait. The trait is itself generic. This is in Rust 1.49.0.

If I do this:

trait Foo<T> {}

struct Baz<F: Foo<T>> {
    x: F,
}

I get a compilation error, because T is not defined. But if I define it:

trait Foo<T> {}

struct Baz<T, F: Foo<T>> {
    x: F,
}

then I get a compiler error because T is unused.

The only option seems to be to include a PhantomData<T> field, but if my generic dependence becomes more complicated, this starts to get more unwieldy:

use std::marker::PhantomData;

trait Foo<T> {}

struct Baz<T, U, F: Foo<T>, G: Foo<U>> {
    phantom_t: PhantomData<T>,
    phantom_u: PhantomData<U>,
    x: F,
    y: G,
}

Half of my fields are phantoms. The struct is practically haunted.

My question is: Is the example at the end that compiles really idiomatic Rust? And if so, why can Rust not detect that the T in Baz<T, Foo<T>> is actually used?

Is the example at the end that compiles really idiomatic Rust?

The idiomatic way to store multiple phantom type parameters is with tuples:

struct Baz<T, U, F: Foo<T>, G: Foo<U>> {
    x: F,
    y: G,
    _t: PhantomData<(T, U)>,
}

why can Rust not detect that the T in Baz<T, Foo<T>> is actually used?

This is actually intended behavior due to variance and the drop check . The idea here is that the compiler needs to know what constraints it can put on the type parameter T , and usage of the PhantomData type will instruct the compiler how it can do so.

You can learn more about PhantomData and how it affects variance in the Rust nomicon .

You should probably do one of these things:

  1. If each struct F could implement Foo in several ways ( Foo<String> , Foo<i32> , etc. for the same F ), simply drop the bound from the struct Baz . The specific choice of T isn't relevant until you decide to use it, but attaching a bound forces you to pick a single T for which the struct will work, even if F would work with multiple types.

    Instead, put the <T> parameter and the F: Foo<T> bound only on the impl blocks where T is used (see Should trait bounds be duplicated in struct and impl? ).

  2. If each struct F is expected to only implement Foo in one way, make T an associated type of Foo instead of a generic type. See When is it appropriate to use an associated type versus a generic type?

You should probably not use PhantomData<T> . This marker type is used not just for variance, as Ibraheem's answer correctly mentions, but also by the drop checker to determine whether Baz<T> logically contains a T , and by the compiler to determine what auto traits should be implemented for Baz<T> . If you are not careful how you use it, you can accidentally overconstrain or underconstrain your API in subtle ways (see Making a struct outlive a parameter given to a method of that struct for one example). Worse, because these properties of a type are visible externally, the choice of PhantomData 's type parameter can be exposed to external code, which makes fixing any mistake a breaking change.

If you don't know which of PhantomData<T> , PhantomData<fn(T)> , PhantomData<fn() -> T> , PhantomData<fn(T) -> T> , PhantomData<*const T> or PhantomData<*mut T> to use (six meaningfully distinct things, and this list is not exhaustive), you should read through the links above and try to determine which kind of variance, drop behavior and auto trait behavior Baz should have with respect to T . These are important qualities to know about a generic struct, and they are part of its external API. If any of them don't make sense for your use case, it probably means that Baz should not be generic over T at all.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM