&Rc, &Arc as method receivers?

#56805 stabilized Rc and Arc as method receivers, making it possible to do something like:

struct Foo;
impl Foo {
    fn foo(self: Arc<Self>) {}
}

However, that forces every call to the method to either have a disposable Arc that won't be used afterwards already, or to Arc::clone, meaning a forced increment and later decrement of the ref counter. (Same with Rc). That can be quite a waste. It gets even worse when the method needs to call other methods that also take self: Arc<Self>.

Stupidly reduced example to illustrate, but the following is not allowed:

struct Foo;
impl Foo {
    fn foo(self: Arc<Self>) {}
    fn bar(self: Arc<Self>) { self.foo(); self.foo(); }
}

And the only correct way, currently, would be:

struct Foo;
impl Foo {
    fn foo(self: Arc<Self>) {}
    fn bar(self: Arc<Self>) { Arc::clone(&self).foo(); self.foo(); }
}

The generated code for fn main() { Arc::new(Foo).bar(); } looks like:

playground::main:
	pushq	%rbx
	subq	$16, %rsp
	movl	$16, %edi
	movl	$8, %esi
	callq	*__rust_alloc@GOTPCREL(%rip)
	testq	%rax, %rax
	je	.LBB5_7
	movq	%rax, %rbx
	movaps	.LCPI5_0(%rip), %xmm0
	movups	%xmm0, (%rax)
	lock		addq	$1, (%rax)
	jle	.LBB5_8
	movq	%rbx, 8(%rsp)
	lock		subq	$1, (%rbx)
	je	.LBB5_3
	movq	%rbx, 8(%rsp)
	lock		subq	$1, (%rbx)
	je	.LBB5_5
(snip)

That is extra useless traffic on the atomic refcounter, and real use-cases would actually have to call Arc::clone too, adding one more lock addq. (BTW, it's worth noting the compiler is not assuming the refcounter is not supposed to be decremented more times than it was incremented, and tests whether the refcount reaches 0 on each decrement, although it's not possible for it to reach 0 on the first decrement, since it was increased first (apart from other unsafe code doing nasty stuff to the refcount)) (BTW2: I'm actually surprised the compiler is not able to figure out this reduced testcase is just doing nothing).

A way to address this would be to allow &Arc as a method receiver.

So, would it make sense to stabilize &Arc and &Rc as method receivers, in advance of the full arbitrary self types support? I guess this should also be extended to &Box and &Pin, actually, for similar reasons. It's actually worse for e.g. Box because it means calling a method with self: Box<Self> will always drop the Self.

1 Like

There's an open PR for this, currently still waiting on FCP being triggered before it can be merged:

3 Likes

A way to address this would be to allow &Arc as a method receiver.

If you don't need a copy of the already-existing Arc, why don't you just take &Self instead of Arc<Self> (or &Arc<Self>)?

5 Likes

The entire point of this is to take ownership, so it seems to require exactly what it should. If the need to have an Arc gets in your way, you want a &self method.

There are usecases where &Arc makes a lot of sense, e.g. if the method may need to take ownership. Instead of pre-cloning the Arc and passing it to the method just to have it dropped, you can pass a reference to an Arc in and it can clone it if it needs to.

7 Likes

A potential alternative might be an equivalent to C++ shared_ptr's enable_shared_from_this helper. If you can guarantee a type is only ever exposed to consumers via an Rc or Arc, then it is safe to provide a method on &self to upgrade the reference.

@nikomatsakis wrote a bit about this pattern as an aside in this gnome-class post.

1 Like

That's exactly what self: &Arc<Self> would be useful for: guaranteeing consumers are only using an Arc.

Would it make sense for std to add a BorrowedArc<'_, T> for this use case (and make it a method receiver)? Yes, you can do this with unsafe and fn new() -> Arc<Self>, but there's no real reason for this not to be a safe pattern, even when only used sometimes.

The advantage of doing this is avoiding the double indirection of &Arc.

// and all of this for `Rc` as well ofc

pub struct Barc<'a, T: ?Sized> {
    ptr: NonNull<ArcInner<T>>,
    phantom: PhantomData<&'a T>,
}

impl<'a, T: ?Sized> Barc<'a, T> {
    pub fn upgrade(this: Self) -> Arc<T> { ... }
}
1 Like

I think the bloat of an additional type here is probably not merited given that the pattern is clearly expressed in &Arc. The one concern I would have is if &Arc could confuse people new to rust into thinking there being only a single indirection.

1 Like

Nested self receivers seem like the way to go for this. In the meantime, for those interested in tinkering with such methods, you can fake add inherent methods on ARc<YourType> through a helper (preferably sealed) trait, so that &Self in these methods is thus &ARc<YourType> (playground).

1 Like