Possible to opt out of copy?

I understand that when assigning a variable to another variable or passing it to a function (not by reference) that ownership is moved unless the type implements the Copy trait, in which case the value is copied. But is it possible to opt out of that in a specific case and say that you explicitly want to move ownership when the type implements the Copy trait? Maybe there is no real reason to ever want to do that, but I just wondered if it is possible. Here's a concrete example:

#[derive(Clone, Copy, Debug)]
struct Point2D {
    x: f64,
    y: f64
}

fn main() {
    let p1 = Point2D { x: 1.0, y: 2.0 };
    let p2 = p1; // p1 is copied, but could I force it to move ownership?
    println!("{:?}", p2);
    println!("{:?}", p1);
}

No, you can't opt out of traits on a case-by-case basis. If you want to convert a Copy type into a non-Copy one, use a newtype wrapper that does not implement Copy.

5 Likes
use std::marker::PhantomData as Ph;

#[derive(Clone, Debug)]
struct ModeClone;

#[derive(Clone, Debug)]
struct ModeCopy;

#[derive(Clone, Debug)]
struct Point2D<Mode = ModeCopy> {
    x: f64,
    y: f64,
    _mode: Ph<Mode>
}
impl<Mode> Point2D<Mode> {
    fn new(x: f64, y: f64) -> Self {
        Point2D {x, y, _mode: Ph}
    }
}

impl Copy for Point2D<ModeCopy> {}

fn main() {
    let p1: Point2D = Point2D::new(1.0,2.0);
    // let p1: Point2D<ModeClone> = Point2D::new(1.0,2.0);
    let p2 = p1;
    println!("{:?}", p2);
    println!("{:?}", p1);
}
3 Likes

If you just make sure to not use p1 after the copy, then it is compiled exactly as if it had been moved, i.e. with a bitwise copy that doesn't use the original value anymore.

The thing that stands out to me about this is that when I'm looking at a function call like this:

do_something(foo, &bar, &mut bar);

I know that bar is passed as an immutable reference and baz is passed as a mutable reference. But I don't know if the ownership of foo is moved or whether the value is copied unless I determine the type of foo and whether that type implements the Copy trait. I'm not saying this is a bad thing, just an observation about the experience of reading code.

1 Like

This is true, but:

  • if it's a type for which it doesn't matter, then… well, then it doesn't matter.
  • if it's a type for which it does matter, then you still can (and probably should) write a newtype wraper.
4 Likes

I think if there is a case where ownership transfer matters then it is incorrect for that type to implement Copy. Copy is for cases where ownership doesn't mean much.

9 Likes

An alternative to newtype:

trait Point2DClone: std::fmt::Debug {}

#[derive(Clone, Copy, Debug)]
struct Point2D {
    x: f64,
    y: f64
}
impl Point2DClone for Point2D {}
impl Point2D {
    fn non_copy(self) -> impl Point2DClone {self}
}

fn main() {
    let p1 = Point2D { x: 1.0, y: 2.0 }.non_copy();
    // let p2 = p1;
    // println!("{:?}", p2);
    println!("{:?}", p1);
}
3 Likes

Agreed, exactly – I was talking more about the situation when all you have is a Copy type and you want to make it non-Copy without reimplementing all of its existing functionality. For instance, if you have primitive integer identifiers that, and you want them to be non-reusable, you may need an Identifier(u64).

I've been looking forward to learning more about how to use the PhantomeData marker in Rust. The use of Phantom Types can be a elegant and powerful approach based on my experience elsewhere. Your example is a nice demonstration of where it can be useful.

I played with your code just a bit. Instead of specifying a default, I treated struct Point2D more strictly like a type constructor (that isn't a concrete type until it's applied with a type parameter), so struct Point2D<Mode> instead of struct Point2D<Mode = ModeCopy>.

Playground

I suspect you're concerned about something you don't need to be concerned about.

One way to think about it is that it's always moving, but if the type is Copy, the original copy is still usable even after moving a copy of it somewhere else. Whether or not your type is Copy, when you pass it to a function, it will still get its memory copied to the location you're moving it to (unless the compiler optimizes that away). It's just that if the type is Copy, then you are still able to use the original, whereas non-Copy types tend to rely on a strict "created once" -> "used" -> "dropped once" lifecycle for correctness.

7 Likes

It's more idiomatic in Rust to not have the Copy trait if the type is not trivial to copy and do clone when you need a copy. If the copy is trivial then there's no good reason to avoid it and do a move instead.

1 Like