Is there a smart pointer like this? (works like a reference by default + gives ownership in a smart way (clones if needed) on request)

Hi,

Let's say we have the following code:

let x = get_event();
let smart_x = SmartPointer::new(x);

if callback_a_exists {
	callback_a(&smart_x)
}

if callback_b_exists {
	callback_b(&smart_x)
}

Sometimes both callbacks are defined, most of the time, however, only one exists.

I want to give these callbacks the ability to request ownership of x if needed (most of the time a reference should be enough).

I don't want to clone x blindly if it's not needed (nor do I want that the callbacks clone x blindly when they want an owned value. If only one callback is defined, cloning would be wasteful).

Here are two example scenarios and what I'm hoping for:

  1. callback a and/or be request shared access to x => smart_x acts like a reference to x

  2. callback a requests ownership of x => SmartPointer returns x to callback a, but keeps a reference to x (which isn't possible, as far as I know). If callback b wants a reference to x, smart_x gives the reference it has. If callback b, however, also requests ownership of x, SmartPointer clones x (from the reference it has) and returns that clone.

Is something like that possible/available?

Most of the time cloning should not be needed, so I don't want to waste resources by cloning x prophylactic.

I thought Cow is what I need, but that seems to be something different.

Yes, &mut Option<T> can do this. Or if you want, just pass a &T and have the recipient clone it if they need a clone.

I'm interpreting your request for ownership to be &mut T exclusive access. I think that's what you mean, but this is different than passing a truly owned T by value.

You can get &mut T with Rc::make_mut, but that will always clone when there's more than one shared owner at the moment. You don't know whether any of the shared readers are looking at the value, so you have to make sure you're totally exclusive to start writing.

With another layer, you can track active readers/writers, using Rc<RefCell<T>>. Both readers and writers would have to try-or-clone, using try_borrow or try_borrow_mut respectively.

1 Like

@alice:

Or if you want, just pass a &T and have the recipient clone it if they need a clone.

What I try to avoid is, that there is only one callback installed (which wants an owned value). In this case, I could give 'x' to the callback directly because there is no other callback that could request ownership to x later (so cloning would be wasteful).

Yes, &mut Option can do this.

You mean, that a callback (that needs an owned value) takes the value out, clones it, and then puts one of the two values back in?

That would have the same problem as above if there is only one callback installed (which will be the case most of time).

I'm interpreting your request for ownership to be &mut T exclusive access. I think that's what you mean,

No, I really mean "owned data". The data doesn't have to be mutated (but parts of the data could get moved out and used for something else).

What I'm working on is a user-facing API for a callback system for events. The event data could be complex, so it's important to me that these events are not cloned needlessly. I don't know what the user needs to do with the data from these events, so I'd like to give the option to get owned data.

I have two callbacks because I also need some internal event handlers for a few events. I could say that internal event handlers get always a reference (and need to clone the event if needed), and user event handlers get the owned value. But then I'd have to duplicate parts of the event handling system, which I'd like to avoid.

For now, I clone the event if there are both event handlers defined, which should not happen very often, but I'd like to avoid this if possible.

If a callback gets the owned version of something that isn't Copy, what could the reference be to?

fn callback_a(x: &Smart<String>) {
    let x = x.get_owned(); // String gets moved out of smart_x into callback_a?
} // x gets dropped here... what does smart_x now refer to?

Wrapping the event in Rc/Arc seems like a reasonable approach here. You can give a cloned copy to each event handler, and they can use try_unwrap and/or make_mut to get owned versions. If only one copy was ever created or the other handlers have already dropped their copies, this will skip the deep clone and use the original instead.

2 Likes

The main problem here is that ordinary ownership in the sense used by Rust implies exclusive access, so you simply cannot give callback_a ownership of the value without cloning it, if you also want to give callback_b any kind of access to it later.

The Arc type seems to be the closest to what you're asking for.

1 Like

@2e71828:

Ah, thanks, that makes sense. @cuviper wrote something similar, but I didn't realize that this is what I need.

So basically, I can write something like this:

if internal_callback_exists && external_callback_exists {
	let x = Arc::new(get_event());
	internal_callback(Arc::clone(x));
	external_callback(x);
} else if internal_callback_exists {
	internal_callback(Arc::new(get_event()))
} else if  {
	external_callback(Arc::new(get_event()))
}

To get an owned value in internal_callback, I'd use Arc::try_unwrap(Arc::make_mut(the_event_arc)), and this would always clone x if both callbacks exist because there still is the original x that is passed to external_callback. Otherwise, there wouldn't be any cloning in the above snippet.

external_callback could directly use Arc::try_unwrap(the_event_arc) to get an owned value without that there is any cloning (as long as I don't send or store the Arc in internal_callback somewhere).

Is that all correct?

That would be a satisfying solution for me :slight_smile:

If a callback gets the owned version of something that isn't Copy, what could the reference be to?

The main problem here is that ordinary ownership in the sense used by Rust implies exclusive access, so you simply cannot give callback_a ownership of the value without cloning it, if you also want to give callback_b any kind of access to it later.

@trentj and @alice:

I was thinking there could be some kind of smart pointer that I haven't heard of that has the ability to somehow do what I need. I'm still new to Rust and I have a strong feeling that I don't grasp some of Rusts concepts fully (for example, I have zero clue what would be possible with unsafe Rust... :wink: ).

Can you give ownership of x to the callback and have it return x?

let x = get_event();
let x = if callback_a_exists {
    callback_a(x)
} else {
    x
};
if callback_b_exists {
   callback_b(x);
}

@alanhkarp: Yes, I could do this. But with the current implementation of my event system, both callbacks need to have the same type (so both callbacks would have to return back the event). Otherwise, I could give a reference to the event to callback_a (the internal event handler), and give the owned value to callback_b.

(and what I mean above, is that there wouldn't be any benefit if both callbacks have to return the event back, as far as I can see).

The reason there can't be a smart pointer that does this is because once you have moved something it's not behind a smart pointer anymore. So the requirement that callback_a can request ownership of x means that smart_x can't hold on to references to x anymore, because callback_a now completely owns it and can decide to drop it. That's what ownership means: the right to destroy. If you don't want callback_a to have the right to destroy x, then it cannot take ownership of x.

I agree with the others that Arc (i.e. shared ownership) is probably your best bet here.

Yes, that looks right to me. You may as well use Arc::try_unwrap(Arc::make_mut(the_event_arc)) everywhere you need ownership, unless cloning would be a catastrophic mistake; it will prevent accidental panics. Also, if the callbacks are all run on the local thread, Rc will avoid the potential overhead of Arc’s atomic operations.

Almost certainly undefined behavior. Safe Rust does incur extra runtime checks in certain situations, but anything is better than diving into unsafe too early. If you have a working solution, that doesn't use unsafe, then you can think about optimization, that involves unsafe code. That's what I recommend, at least.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.