1

I have a fairly simple use case where I store my own generic KeyValuePair struct in a Vector, declared like this:

#[derive(Eq, Ord, PartialEq, PartialOrd)]
pub struct KeyValuePair<TKey, TValue> {
    key : TKey,
    value : TValue
}
pub struct OtherStruct<TKey, TValue> 
where TKey : Hash + Eq + Ord
{
    values : Vec<KeyValuePair<TKey, TValue>>
}

This is all great, but I'm having trouble figuring out how to use Vector's binary_search_by_key function with this setup. No matter what I pass the ownership checker seems to get angry. I understand some of the exception messages, but I'm still stuck on how to accomplish what I actually want.

I started with:

match &self.values.binary_search_by_key(key, |kvp| kvp.key) {
    Ok(pos) => return self.values.get(*pos),
    Err(pos) => return None
}

This gives me the error:

error[E0507]: cannot move out of `kvp.key` which is behind a shared refere

This one makes sense; as written it would have to move the return value. So I changed the return to a borrow:

match &self.values.binary_search_by_key(key, |kvp| &kvp.key) {

This one fails because the closure function definition expects TKey as the return type, not &TKey


^^^^^^^^ expected type parameter `TKey`, found `&TKey`

My next attempt was to borrow the parameter to avoid the move, but I got this:


match &self.values.binary_search_by_key(key, |&kvp| kvp.key) {
|                                             ^---
|                                             ||
|                                             |data moved here
|                                             |move occurs because `kvp` has type `KeyValuePair<TKey, TValue>`, which does not implement the `Copy` trait
|                                             help: consider removing the `&`: `kvp`

This one doesn't make any sense to me; I don't get why borrowing kvp would cause a move? Isn't it the other way around - it would normally move, but adding & borrows instead?

What is the proper syntax to search the vector by key without breaking ownership?

PitaJ
  • 12,969
  • 6
  • 36
  • 55
Tim Copenhaver
  • 3,282
  • 13
  • 18

1 Answers1

3

In the future, please provide your whole function definition.

I assume it was something like this:

impl<TKey, TValue> OtherStruct<TKey, TValue>
where TKey : Hash + Eq + Ord
{
    fn get(&self, key: &TKey) -> Option<&KeyValuePair<TKey, TValue>> {
        match self.values.binary_search_by_key(key, |kvp| &kvp.key) {
            Ok(pos) => return self.values.get(pos),
            Err(pos) => return None
        }
    }
}

The type mismatch is between the first and second parameter passed to binary_search_by_key.

pub fn binary_search_by_key<'a, B, F>(
    &'a self,
    b: &B,
    f: F
) -> Result<usize, usize>
where
    F: FnMut(&'a T) -> B,
    B: Ord,

It expects the first parameter to be type &B, and the closure to return type B. So since you need to return &TKey from the closure, you need to pass &&TKey as the first argument:

impl<TKey, TValue> OtherStruct<TKey, TValue>
where TKey : Hash + Eq + Ord
{
    fn get(&self, key: &TKey) -> Option<&KeyValuePair<TKey, TValue>> {
        match self.values.binary_search_by_key(&key, |kvp| &kvp.key) {
            Ok(pos) => self.values.get(pos),
            Err(pos) => None
        }
    }
}

You also don't need the returns at all.

playground

PitaJ
  • 12,969
  • 6
  • 36
  • 55
  • Thanks, that is exactly my function definition. Didn't really think of it being relevant, but then I never considered double-borrowing to be a thing. I haven't seen that in the docs and can't really wrap my mind around what it does in practice. I'll go do some digging though, this explains a lot. – Tim Copenhaver Nov 30 '22 at 20:49
  • @TimCopenhaver it's a [reference to a reference](https://stackoverflow.com/questions/28519997/what-are-rusts-exact-auto-dereferencing-rules). Probably closer to a double pointer than a "double borrow". – Jared Smith Dec 01 '22 at 14:27
  • LMK if this should be a separate question. But my confusion comes from the fact that key (as a reference) is already allocated on the stack. So the second reference stores the address on the stack where &key is, rather than a heap address? In C stack pointers are dangerous since it can go out of scope. Is the rust compiler just smart enough to guarantee &key's lifetime outlasts &&key so the stack pointer is safe? It seems difficult with closures in particular – Tim Copenhaver Dec 01 '22 at 19:39
  • I guess put another way, what happens if I returned a closure from the function. &key would go out of scope when the stack frame is popped...wouldn't that make &&key corrupt? – Tim Copenhaver Dec 01 '22 at 19:45
  • 1
    The Rust compiler is indeed smart enough to know when a reference to a value on the stack is valid. You will get an error if the compiler cannot prove a given reference does not live beyond the lifetime of the value it points to. – PitaJ Dec 01 '22 at 20:12