Struct containing reference to slice does not implement copy

Hello,
I'm currently struggle with implementing Copy for a struct containing a reference. It seems to have an implicit bound on T being Copy which I don't understand since I'm only working with references and not with T itself.

The struct is defined like this:

#[derive(Eq, PartialEq, Copy)]
struct SmartSlice<'a, T> {
    slice: &'a [T]
    start: usize,
    end: usize
}

which I use in a method like this:

pub fn merge(&self, other: &SmartSlice<'a, T>) -> SmartSlice<'a, T> {
    // ...
    if other.is_empty() {
        return *self;
        // ERROR: cannot move out of `*other` which is behind a shared reference [E0507]
        // move occurs because `*other` has type `SmartSlice<'_, T>`, which does not implement the `Copy` trait
    }
    // ...
}

Why does the compiler require T to be copy here?

Shouldn't references always implement copy (since it's only a reference) and shouldn't therefore my struct also implement copy (without any bound on T implementing Copy) since it only works with references and never copies T itself?

I also tried adding a Self: Copy bound to the method, but if I use it with where T=String I get the following error when I try to call the method:

Trait Copy ist not implemented for "String"

which is clear, but still I'm only working with references to String and not with String itself, i.e. the value wouldn't be copied by the operation above.

#[derive] is not particularly smart. It sees a <T> generic on the struct, so it just requires that T implements Copy.

You'll have to manually implement Copy yourself.

1 Like

That's just how the derive macros work, they're predicated on generics having the same bound. Changing that now would take away some flexibility from current consumers (due to SemVer hazards), so "perfect derive" will probably/hopefully be a distinct macro. (Perfect derive hasn't always been viable.)

In the meanwhile, write out your own implementation without the bounds instead of deriving,

2 Likes

Would "perfect derive" not require lifetimes to be the same too, or would it essentially treat lifetimes like "normal" type parameters thus not generalize to different "types" (i.e., it seems reasonable for Foo<T> to not implement PartialEq<Foo<T2>> where T: PartialEq<T2>, but it is less reasonable when applied to lifetimes)?

OP, you derived PartialEq, which means you only implemented PartialEq such that the left and right operands are the exact same type. Normally this won't be a problem due to subtyping and covariance; but in higher-level contexts, this won't help. Additionally, even though core blanket implements PartialEq<&A> for &B, PartialEq<&mut A> for &mut B, PartialEq<&A> for &mut B, and PartialEq<&mut A> for &B (with the obvious bounds), something like below won't compile due to the invariance of &mut T (i.e., covariance won't save you):

#[derive(PartialEq)]
struct SmartSlice<'a, T> {
    slice: &'a [T],
    start: usize,
    end: usize,
}
fn foo(x: &mut SmartSlice<'_, ()>, y: &SmartSlice<'_, ()>) -> bool {
    // This doesn't compile since the lifetimes may be different,
    // and `&mut T` is invariant with respect to `T`.
    // The "workaround" is quite simple here though: `*x == *y`.
    x == y
}
fn bar(x: &SmartSlice<'_, ()>, y: &SmartSlice<'_, ()>) -> bool {
    // This compiles even though the lifetimes may be different,
    // but only because `&T` is covariant with respect to `T` and
    // it's guaranteed that the referent type of `x` is either a subtype
    // or supertype of the referent type of `y` (i.e., covariance applies).
    x == y
}

In contrast a manual implementation will cause the code to compile:

struct SmartSlice<'a, T> {
    slice: &'a [T],
    start: usize,
    end: usize,
}
// If you want to generalize even further, you would introduce another type parameter,
// `T2`, such that `T: PartialEq<T2>`.
impl<T: PartialEq> PartialEq<SmartSlice<'_, T>> for SmartSlice<'_, T> {
    fn eq(&self, _: &SmartSlice<'_, T>) -> bool {
        true
    }
}
fn foo(x: &mut SmartSlice<'_, ()>, y: &SmartSlice<'_, ()>) -> bool {
    x == y
}

I realize this is not technically relevant to what you explicitly asked, but it's relevant to the general idea that derive isn't as smart as you may think or desire.

1 Like

To me that seems like a concern orthogonal to the topic of the article, which was about the trait abilities of fields. (What's planned for any feature as a whole, I couldn't say.)

Are there even any std derives that specify a type parameter? The derivable parameterized traits I can think of have default type parameters.

I agree, so perhaps I shouldn't have asked the question. I should have simply quoted you and stated that "even with a 'perfect derive', one may still find themselves in a situation where it's not 'perfect' enough and still need to implement the trait manually."

That's out of my wheelhouse. Macros will likely forever be pretty foreign to me. While I do leverage some basic ones (e.g., vec!, derive, etc.) and have even written a handful of simple ones, Rust is "complex" enough that I haven't put much time or effort in developing a deeper understanding of the macro system.

1 Like

I agree with that, and pointing out the limitations of deriving PartialEq was also a good call :+1:.

I had a look and there's a couple unstable ones.

  • From<Field> which uses the exact type of the field
  • CoercePointee which generates some type-generic implementations based on a structs type parameter

I don't think the second one is relevant here,[1] but for fun we can think about how From could be expanded.

#[derive(From)] struct Str<'a>(&'a str);
// Easy enough (? no idea how feasible this really is)
impl<'a, '__lt: 'a> From<&'__lt str> for Str<'a> { ... }

#[derive(From)] struct Func<'a>(fn(&'a str));
// Still easy enough?
impl<'a: '__lt, '__lt> From<fn(&'__lt str)> for Func<'a> { ... }
// But shouldn't we get this one too?
// (n.b. fires a coherence_leak_check lint, but this use case is expected to
// be supported forever as I understand things.)
impl<'a> From<fn(&str)> for Func<'a> { ... }

#[derive(From)] struct Tup<'a>((fn(&'a str), &'a str));
// `Tup<'a>` is invariant in `'a`, but the fields of `(T, U)` coerce independently
impl<'a: '__lt0, '__lt0, '__lt1: 'a> From<(fn(&'__lt0 str), &'__lt1 str)> for Tup<'a> { ... }
// And the HR one
impl<'a, '__lt1: 'a> From<(fn(&str), &'__lt1 str)> for Tup<'a> { ... }

Those are all subtype based, but we could also try to throw in unsizing coercions (at least when a field involves a dyn Trait or array), which

  • can happen through layers in some cases
  • can change dyn lifetimes
  • can drop dyn auto-traits and coerce to supertraits in a combinatorial fashion

Aside from compile time, this might result in generating every possible vtable instead of just the ones you actually constructed? Not sure.


Anyway, the vibe I get is that there's probably always going to be something that a reasonable derive still misses, so "derive isn't smart enough" will probably always be a thing. Though perhaps it could be smarter or evolve knobs for common shortcomings like PartialEq on a lifetime bearing struct.

#[derive(PartialEq<SmartSlice<'_, T>>)]
struct SmartSlice<'a, T> { ... }

#[derive(PartialEq)]
struct SmartSlice<#[derive_wildcard(PartialEq)] 'a, T> { ... }

With knobs instead of "I'll do what I can", at least it's more clear where the limitations lay.


  1. or at least I'm unwilling to go explore enough to find out just now :wink: ↩︎

2 Likes

This opened a can of worms I wasn't aware of. I incorrectly assumed the two From impls for Func would be a hard error due to overlap since for<'a> fn(&'a str) is a subtype of fn<'a>(&'a str), but the compiler errs when you only have the second impl but pass a for<'a> fn(&'a str) into from. You have to instead manually coerce the for<'a> fn(&'a str) into a fn(&'a str) before calling from. Mind blown. Yet again you have illuminated me.

P. S. Thanks for the use of "n.b.". It took me an embarrassing amount of time to figure out what that meant. At first I thought it was something to do with the compiler like negative-bounds checking or something, then it dawned on me to look up a more general use of that abbreviation and I found Nota bene.

1 Like

They wanted it that way at some point, but allowing it had slipped through and at least one widely-used crate was relying on the ability, so from what I understand it will remain supported. That said, it's been some time and there hasn't been an FCP on the matter yet and the future compat warning remains. :person_shrugging: