Why is Rc::as_ptr impl for ?Sized, but not Weak::as_ptr?

I noticed that Rc::as_ptr is implemented for T: ?Sized, but Weak::as_ptr is only implemented for T (without the ?Sized constraint). Is this difference between Rc and Weak intended?

For context, I have types with Rc<[T]> and Weak<[T]>, and I'm trying to create an "identity" function by casting a pointer to the array's first element to usize, but I can't use Weak::as_ptr since [T] is unsized. Is there a better way to do this?

Possibly because there’s currently no good way to create a null/uninitialized pointer for a generic unsized type. (It’s possible for slices but not for trait objects.)

You could write your own as_ptr for slice pointers, though it may be a bit more expensive than the standard one, because it calls upgrade. On the plus side, it works on the stable channel:

use std::{ptr::{slice_from_raw_parts, null}, rc::Weak};

pub fn as_ptr<T>(w: &Weak<[T]>) -> *const [T] {
    match w.upgrade() {
        Some(strong) => &*strong,
        None => slice_from_raw_parts(null(), 0),
    }
}

A more efficient version could be added to the standard library as a method on Weak<[T]>. If you need this, you should add a comment about your use case to the tracking issue.

I thought about using that, but not only is it expensive, it also isn't technically "correct". When all the strong references are dropped, the identity function will always return 0. See the example below. I will comment on the issue you linked, thanks!

use std::rc::{Rc, Weak};

fn rc_identity<T>(t: &Rc<T>) -> usize {
    Rc::as_ptr(t) as usize
}

fn weak_identity<T>(t: &Weak<T>) -> usize {
    Weak::as_ptr(t) as usize
}

fn weak_identity_workaround<T>(t: &Weak<T>) -> usize {
    match t.upgrade() {
        Some(strong) => rc_identity(&strong),
        None => 0
    }
}

// without workaround
fn case_a() {
    let strong: Rc<u8> = Rc::new(0);
    let weak = Rc::downgrade(&strong);
    let identity = rc_identity(&strong);
    assert!(identity == weak_identity(&weak));
    drop(strong);
    assert!(identity == weak_identity(&weak));
}

// with workaround
fn case_b() {
    let strong: Rc<u8> = Rc::new(0);
    let weak = Rc::downgrade(&strong);
    let identity = rc_identity(&strong);
    assert!(identity == weak_identity_workaround(&weak));
    drop(strong);
    // notice, the identity is broken when strong is dropped
    assert!(identity != weak_identity_workaround(&weak));
}

fn main() {
    case_a();
    case_b();
}

Note that Weak::as_raw also isn't guaranteed to produce the results you want, in the case where there are no strong references, though it will as currently implemented, and perhaps such a guarantee could be added. For now, the docs just say:

The pointer is valid only if there are some strong references. The pointer may be dangling, unaligned or even null otherwise.

Out of curiosity, what is your use case for needing to track the identity of a Weak after the value it pointed to has been destroyed?

Also, your code could produce identity collisions, if a new Rc happens to be allocated at the same location where an old one was deallocated: This is incorrect as long as both Weak are still in scope; see below.

let strong0: Rc<u8> = Rc::new(0);
let weak0 = Rc::downgrade(&strong0);
drop(strong0);

let strong1: Rc<u8> = Rc::new(1);
let weak1 = Rc::downgrade(&strong1);

// Fails if `strong1` happens to have the same address as `weak0`!
assert_ne!(weak_identity(&weak0), weak_identity(&weak1));

(Weak::as_ptr is stable on beta.)

On the impl and stabilization for Weak::as_ptr I brought up the need for it to support T: ?Sized as well. The fact that it doesn't is "just" an implementation limitation, and is planned to be loosened in the future.

In fact, I think it might be possible to loosen it now (bit not when it was stabilized) because of the added align_of_val_raw.

TL;DR: the way that Weak::as_ptr goes from *mut RcBox<T> to *mut T requires getting the alignment of T. This is possible statically for sized T via mem::align_of::<T>(). For unsized T, this used to require creating a reference an calling mem::align_of_val(&T), which requires an actual T behind the reference. This isn't guaranteed for Weak because of Weak::new (and weak handles to dropped Ts, but std potentially is privileged enough to get away with a reference to a dropped type; you aren't). We now have (unstable) mem::align_of_raw(*const T), which allows getting the required alignment of a pointer without asserting its validity, which makes Weak::as_raw implementable for unsized T.

If you followed that, feel free to submit a PR! I'll potentially get around to submitting a PR if nobody else has later this week, since this is one of my pet peeves with the stabilized API.

Technically this would stabilize that "something like" mem::align_of_val(*const T) is possible, so it would be fair to block relaxing the bound to ?Sized on that, but I don't think that's necessary.

The current Rc implementation prevents this, as the inner RcBox allocation is only freed after all of the Weaks are dropped.

Plus, I think the exposed API prevents dropping the inner allocation sooner. Rc is required to be able to roundtrip successfully through from_raw(into_raw(_)), which rules out a split allocation like is done by C++ std::shared_ptr. I suppose Weaks could use an internal Cell to change to a dangling Weak on a failed upgrade, as weak_count always returns 0 if there are no strong references left.

(This is... problematic for Weak::from_raw. The weak count is (exposed as) 0, so Weak::from_raw(Weak::into_raw(weak)) is "documented UB" (but not (yet) implemented UB) for a weak reference outliving strong references. This is, at best, a problematic footgun, and at worst, makes Weak::from_raw unusable. Issue tracker: #73840)

3 Likes

Oh, right. I should have known this but forgot.

I got antsy and had a bit of extra floating time, so filed #73845. This does the internal adjustments required to support unsized T in Weak::as_ptr, but does not do the actual relaxation of bounds. (That will be a follow-up PR which will (probably) need T-libs FCP signoff, but this PR just needs a r+ as an internal only change.)

1 Like

#73845 is merged; #74022 is filed to actually relax the bounds on these functions.

EDIT: I messed up, we're not quite ready for that PR yet. I'm working on it, though.