Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to prevent Weak::new() from over allocating?

Tags:

rust

Follow-up of Why doesn't Weak::new() work when Rc::downgrade() does?

When attempting to implement Weak::new() in a way that would NOT require it to allocate memory for the underlying type even though it is never going to be used, I hit a roadblock.

The definition of RcBox<T> is rather simple:

struct RcBox<T: ?Sized> {
    strong: Cell<usize>,
    weak: Cell<usize>,
    value: T,
}

And the goal here is to create a RcBox<T> which will actually NOT contain any value. Essentially, a RcBox<()>.

However, there is a snag. *mut RcBox<()> is a thin pointer but *mut RcBox<T> is potentially a fat pointer. We have the data part of this fat pointer right, but there are many different cases of fat pointers so attempting to synthetize the rest is hard.

As can be seen in the linked question, I can make it work for just trait objects:

impl<T: ?Sized> Weak<T> {
    pub fn new() -> Weak<T> {
        unsafe {
            let boxed = Box::into_raw(box RcBox {
                strong: Cell::new(0),
                weak: Cell::new(1),
                value: (),
            });

            let ptr = if size_of::<*mut ()>() == size_of::<*mut T>() {
                let ptr: *mut RcBox<T> = transmute_copy(&boxed);
                ptr
            } else {
                let ptr: *mut RcBox<T> = transmute_copy(&TraitObject {
                    data: boxed as *mut (),
                    vtable: null_mut(),
                });
                ptr
            };

            Weak { ptr: Shared::new(ptr) }
        }
    }
}

However this won't work with str (for example).

I made another attempt trying to isolate the fixed-size portion of RcBox while letting the compiler infer the fat part of the pointer:

struct RcBox<T: ?Sized> {
    counters: RcBoxCounters<T>,
    value: T,
}

struct RcBoxCounters<T: ?Sized> {
    strong: Cell<usize>,
    weak: Cell<usize>,
    _phantom: PhantomData<T>,
}

impl<T: ?Sized> Weak<T> {
    pub fn new() -> Weak<T> {
        unsafe {
            let boxed = Box::into_raw(box RcBox::Counters::new(0, 1));
            Weak { ptr: Shared::new(boxed as *mut RcBox<T>) }
        }
    }
}

which sounds very clever until the compiler squashes your enthusiasm:

error[E0375]: implementing the trait `CoerceUnsized` requires multiple coercions
  --> <anon>:58:40
   |
58 | impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<RcBox<U>> for RcBox<T> {}
   |                                        ^^^^^^^^^^^^^^^^^^^^^^^ requires multiple coercions
   |
   = note: `CoerceUnsized` may only be implemented for a coercion between structures with one field being coerced
   = note: currently, 2 fields need coercions: counters (RcBoxCounters<T> to RcBoxCounters<U>), value (T to U)

That is:

  • I think that to let the compiler synthesize the fat part I need a PhantomData in RcBoxCounters,
  • however doing so requires 2 conversions for the coercion, which is not allowed.

So, is there a way to fix Weak::new() so that it stops allocating extraneous (unnecessary) memory?

Note: I do mean allocating only space for the two counters, allocating large and trimming afterward does NOT help.

Note: It has been remarked that one could use an Option or special value to denote the absence of value. This requires branching on each method, which may not be desirable. I prefer learning to fiddle with fat pointers.

like image 993
Matthieu M. Avatar asked Jun 15 '17 18:06

Matthieu M.


People also ask

Should you avoid using new C++?

The new operator should only be used if the data object should remain in memory until delete is called. Otherwise if the new operator is not used, the object is automatically destroyed when it goes out of scope.

Does C++ allocate memory automatically?

In C language, we use the malloc() or calloc() functions to allocate the memory dynamically at run time, and C++ also supports these functions. But, in C++, allocation and deallocation are done manually.

How do you dereference a weak pointer in C++?

but... you cannot dereference a weak pointer. you have to lock it, get a shared one from it and use that one. You are supposed to convert/create a shared_ptr from them before accessing the raw pointer to avoid race conditions.

Is C++ memory management hard?

Memory management in C++ doesn't make the language hard, it makes it fragile, their is a big difference. The concepts behind memory management really aren't rocket science, it's the kind of thing you can learn in an afternoon.


1 Answers

Yes, there is a way, and it was actually submitted to the standard library:

  • Arc: remove unused allocation from Weak::new()
  • Rc: remove unused allocation and fix segfault in Weak::new()

This change makes it so that Weak::new() allocates no memory at all. Instead, it is created with a null pointer. The only things done with a Weak are trying to upgrade, cloning, and dropping, meaning there are very few places that the code actually needs to check if the pointer is null.

like image 151
Shepmaster Avatar answered Oct 22 '22 09:10

Shepmaster