Why does std::rc::Weak allocate?


#1

I read documentation about std::rc::Weak:

fn new() -> Weak<T>1.10.0	[src][−]
Constructs a new Weak<T>, allocating memory for T without initializing it

And can not understand why it allocate memory? By default without connection with
std::rc::Rc it should be like None. Why it allocate memory?


#2

Weak is essentially the same as an Rc modulo not keeping the value alive on its own. In particular, you can upgrade() it to an Rc without any extra allocation or copying.

What’s the goal behind using a “standalone” Weak?


#3

To prevent Option<Weak>.
Weak already have two state (is possible convert to Rc and it is impossible).

So in some function I want return Weak::default()/Weak::new() if no Rc available,
but to prevent redundant memory allocation, I have to return Option<Weak>,
and introduce instead of 2 variants, 4 variants only 2 of them have sense.


#4

What do you mean by “no Rc available”? Are you not starting out with an Rc to begin with? I’ve not really seen any standalone Weak usage - it’s usually obtained from downgrading an existing Rc.


#5

Consider such pseudo code:

struct Foo {
  a: Vec<Rc<RefCell<u32>>>,
}
impl Foo {
fn foo(&self, index: usize) -> Weak<RefCell<u32>> {
  if index < self.a.len() {
    return a[index].downgrade();
  } else {
    return Weak::default();
  }
}
}

in real code I have to return Option<Weak>, because of redundant allocation.


#6

What are the semantics/intention of returning a Weak::default() there? An Option<Weak<...>> makes more sense to me anyway - why manufacture a “fake” weak if there’s no Rc it’s attached to?


#7

Ordinary semantic, I thought - "I give you weak reference, so check it every time you use it and forget about it, when no Rc behind it`.

So user of this function should handle two variants. In every moment Weak may become invalid
because of Foo::a resize, so I can not see any difference between index out of scope and ordinary return.

Why I should force user to handle 3 variants: Some(Weak with no RC), Some(Weak with RC), None,
instead of 2 Weak with no RC and Weak with RC?


#8

For those semantics, I’d personally wrap this in a “handle” style API and hide the states from the user:

struct Handle<T> {
  inner: Option<Weak<T>>,
}

impl<T> Into<Option<Rc<T>>> for Handle<T> {
    fn into(self) -> Option<Rc<T>> {
        self.inner.and_then(|w| w.upgrade())
    }
}

#9

I took a look into the source code what new() even does:

    #[stable(feature = "downgraded_weak", since = "1.10.0")]
    pub fn new() -> Weak<T> {
        unsafe {
            Weak {
                ptr: Shared::from(Box::into_unique(box RcBox {
                    strong: Cell::new(0),
                    weak: Cell::new(1),
                    value: uninitialized(),
                })),
            }
        }
    }

As I understand it, both Rc<T> and Weak<T> need to point to some memory block which counts how many weak and how many strong pointers there are. This memory block also stores the value.

It looks like there is no way around the allocation based on the implementation of Rc and Weak.

I was wondering, how anything can be deallocated when all strong references are dropped. Because the memory location where the Weak<T> pointers point needs to stay valid. If there are weak pointers remaining, then only the inner value is dropped. A Rc<u64> stores all information in .value and nothing happens to it. But a Rc<String> can drop its content. The capacity, length and (invalid) pointer will stay in memory until the last weak reference is dropped.

Source: https://doc.rust-lang.org/src/alloc/rc.rs.html


#10

The allocated memory is uninitialized. Pretty much of no harm. If you want to speed up code (one way;) cloning an existing Weak will avoid extra allocation.

Once the last Rc is dropped the value is dropped but not deallocated.
https://doc.rust-lang.org/src/alloc/rc.rs.html#838
When last Weak is dropped the allocation with uninitialised value is dealloc.
https://doc.rust-lang.org/src/alloc/rc.rs.html#1270


#11

In addition to the things other people have mentioned, by always allocating (instead of, say, allowing a null pointer internally), it’s never null, and thus gets layout-optimized so the sizes of Option<Rc<T>> and Rc<T> are the same – letting people choose between allocate-for-empty and null-check-each-access themselves without overhead.