Does a member function take ownership of a self argument?

It is my understanding that if you pass self to a member function, the object corresponding to self is moved to the member function.

Looking at

we see the following prototype

pub fn cos(self) -> f32

Here is an example case where self does not get consumed:

fn main() {
    let angle  = 0.0f32;
    let cosine = angle.cos();
    println!( "angle = {angle}, cosine = {cosine}");
}

f32, like many primitive types, is Copy.

when a type is Copy, what normally would be a "move" operation (variable binding, function argument passing, move closure, etc) will implicitly create a clone bitwise copy, instead of being "consumed".

5 Likes

So why use a self instead of &self argument to cos ? The effect is the same and the meaning is more explicit. Is &self slower due to de-referencing ?

there are no practical differences between a function taking arguments of type &f32 and f32 in terms of performance.

I think it's mostly due to convention: simple scalar types such as bool, i32 or f32 are typically passed by value. examples inlcudes inherent methods on these types, such as i32::is_even() and f32::cos(), etc.

in some cases, they are passed by reference, but these are usually because of the API or implementation uses generics. for example, <f32 as PartialEq<f32>>::eq() has type signature of fn (&f32, &f32) -> bool.

So if one is implementing cos for a type that does not implement Copy, they should use &self to get behavior that is similar to self for f32.

1 Like

what do you mean by "similar" behavior? if you mean not consuming the argument, then yes, you can use &self [1]. but that really depends what your cos() function does.

also, semantics matters. for example, i32::is_even() checks whether an integer value is even or odd, it does not check whether a reference [2] is even or odd (what does that even mean?).

you should also consider calling the function NOT using method call syntax. compare these hypothetical example:

// by value
f32::cos(3.1416);
i32::is_even(x + y);

// by ref
f32::cos(&3.1416);
i32::is_even(&(x+y));

  1. because a shared reference type &T is always Copy, regardless whether T is Copy or not ↩︎

  2. a.k.a. a shared access ↩︎

If you are implementing cos generically for types that may not be copy, then you would type it as cos(&self), because yes, you don't want to move the value when its not copy. A good example of this from std is when using Hashmap::get, even with a Hashmap<u64, u64> you still have to pass in a &u64 and get back an Option<&u64>, because Hashmap deals with non-copy types as well as copy types.

Like @nerditation pointed out though they aren't exactly the same semantically - one is borrowing and one isn't. Like they said too: what does "is this ref to a u64 even" even mean? (pedantically speaking).

On 64-bit platforms &f32 is larger than f32. When the function call literally takes &f32, it will require indirection, and won't use float-specific calling convention (which could have faster option for passing floats).

In practice a call to cos will get inlined and the optimizer will remove any difference. But in cases where it doesn't get optimized out (e.g. you use function pointers dynamically), there will be a slight difference in performance.

8 Likes

that's not true &f32 means that a call to that function will expect to recieve an address to an area of memory from which it can load an f32 value while f32 passes the value directly.
what asm the compiler exactly generates afterwards depends on a lot of stuff but it's definetly a different starting point which can affect it's ability to optimize stuff

I have run into a related problem with the Neg trait

It appears to me that if I am using a type that does not implement copy, and I have a non mutable version of the argument to the negative operator, I need to clone it before taking the negative ?

Here is an example of the necessary clone:

use std::ops::Neg;

#[derive(Debug, Clone)]
struct VecInt {
    pub data : Vec<isize>,
}
impl Neg for VecInt {
    type Output = Self;

    fn neg(mut self) -> Self {
        for element in self.data.iter_mut() {
            *element = - *element
        };
        self
    }
}
fn main() {
    let v1 = VecInt{ data : vec![1,2] };
    let v2 = - v1.clone();
    assert_eq!( v1.data, vec![1,2] );
    assert_eq!( v2.data, vec![-1,-2] );
}

Not necessarily. You can always implement the trait on a reference to your type.

impl Neg for &VecInt {
    type Output = VecInt;

    fn neg(self) -> Self::Output {
        VecInt {
            data: self.data.iter().map(|e| -*e).collect(),
        }
    }
}

fn main() {
    let v1 = VecInt { data: vec![1, 2] };
    let v2 = -&v1;
    assert_eq!(v1.data, vec![1, 2]);
    assert_eq!(v2.data, vec![-1, -2]);
}
1 Like

Well, do you? That depends on if you need the non-negated version afterwards. Meanwhile taking a reference would force that clone on the caller regardless.

1 Like

I like your idea, but I am having trouble implementing it in generic code; e.g.,

use std::ops::Neg;

struct MyVec<T> {
    pub data : Vec<T>,
}
impl<T> Neg<T> for &MyVec<T> where for<'a> &'a T: Neg
{
    type Output = MyVec<T>;

    fn neg(self) -> MyVec<T> {
        MyVec {
            data: self.data.iter().map(|e| -e).collect(),
        }
    }
}
fn main() {
    let v1 = MyVec { data: vec![1, 2] };
    let v2 = -&v1;
    assert_eq!(v1.data, vec![1, 2]);
    assert_eq!(v2.data, vec![-1, -2]);
}

results in

src/main.rs:6:9
  |
6 | impl<T> Neg<T> for &MyVec<T> where for<'a> &'a T: Neg
  |         ^^^^^^     ^^^^^^^^^                      --- unsatisfied trait bound introduced here
  = note: 126 redundant requirements hidden
  = note: required for `&MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<MyVec<...>>>>>>>>>>>>>>>>>>>>` to implement `std::ops::Neg`
...

Neg doesn't even take a generic parameter, and neither do you need a HRTB here:

use std::ops::Neg;

struct MyVec<T> {
    pub data : Vec<T>,
}
impl<'a, T> Neg for &'a MyVec<T> where &'a T: Neg<Output = T>
{
    type Output = MyVec<T>;

    fn neg(self) -> MyVec<T> {
        MyVec {
            data: self.data.iter().map(|e| -e).collect(),
        }
    }
}

Thanks.
The Neg<T> was a typo. One gets the same error message if you change it to Neg . In other words, the error message did not indicate the problem. I think that the real problem is that the Neg output type need to be in the where clause.