Implementing a Custom Reference Counted Pointer (Rc) in Rust
This challenge asks you to implement a simplified version of Rust's Rc (Reference Counted) smart pointer. Rc allows multiple owners of a single piece of data, automatically managing the lifetime of the data by tracking the number of references. Implementing your own Rc will deepen your understanding of ownership, borrowing, and memory management in Rust.
Problem Description
You are to implement a custom Rc type named MyRc. This type should provide the core functionality of Rc: shared ownership of a value and automatic deallocation when the last reference is dropped. Your MyRc should:
- Hold a pointer to a value: The
MyRcshould store a pointer to the data it manages. - Maintain a reference count: A counter should track the number of
MyRcinstances pointing to the same data. - Provide a way to access the underlying data: A
get()method should allow access to the data, returning anOption<&T>(to handle the case where theRchas been dropped and the data deallocated). - Handle deallocation: When the last
MyRcinstance pointing to the data is dropped, the data should be deallocated. - Cloneable:
MyRcshould implement theClonetrait. Cloning should increment the reference count. - No Interior Mutability: Your
MyRcshould not provide a way to mutate the underlying data. It's purely for shared, immutable access.
Key Requirements:
- Use raw pointers and
unsafeblocks where necessary to manage memory directly. This is crucial for demonstrating the underlying mechanisms ofRc. - Ensure memory safety. Avoid dangling pointers and double-frees.
- The data should be allocated on the heap.
- The
get()method should returnNoneif theRchas been dropped and the data deallocated.
Examples
Example 1:
Input:
let data = 5;
let rc1 = MyRc::new(&data);
let rc2 = rc1.clone();
assert_eq!(rc1.get(), Some(&5));
assert_eq!(rc2.get(), Some(&5));
Output:
rc1.get() and rc2.get() both return Some(&5)
Explanation: Both rc1 and rc2 point to the same data (5), and the reference count is 2.
Example 2:
Input:
let data = "hello";
let rc1 = MyRc::new(&data);
let rc2 = rc1.clone();
drop(rc1);
assert_eq!(rc2.get(), Some(&"hello"));
Output:
rc2.get() returns Some(&"hello")
Explanation: rc1 is dropped, decrementing the reference count to 1. rc2 still points to the data.
Example 3: (Edge Case - Deallocation)
Input:
let data = 10;
let rc1 = MyRc::new(&data);
let rc2 = rc1.clone();
drop(rc1);
drop(rc2);
let rc3 = MyRc::new(&data); // Attempt to create a new Rc after deallocation
assert_eq!(rc3.get(), None);
Output:
rc3.get() returns None
Explanation: rc1 and rc2 are dropped, deallocating the data. rc3 attempts to create a new Rc pointing to deallocated memory, which should return None.
Constraints
- Memory Allocation: The data must be allocated on the heap using
std::alloc::allocand deallocated usingstd::alloc::dealloc. Do not useBox. - Pointer Size: Assume a 64-bit architecture.
- Reference Count Size: The reference count should be stored in a
usize. - No External Crates: You are not allowed to use any external crates beyond the standard library.
- Safety: The code must be memory-safe. Panics due to memory errors are unacceptable.
- Performance: While not the primary focus, avoid unnecessary allocations or copies.
Notes
- Start by defining the
MyRcstruct, which should contain a raw pointer to the data and a raw pointer to the reference count. - The
new()function should allocate memory for the data and the reference count, initialize the reference count to 1, and return aMyRcinstance. - The
clone()method should increment the reference count. - The
get()method should safely dereference the raw pointer to the data, returning anOption<&T>. - The
drop()method should decrement the reference count. If the reference count reaches 0, it should deallocate the data and the reference count. - Remember to use
unsafeblocks appropriately when working with raw pointers. Carefully consider the lifetime implications of your code. - Consider using a helper function to handle the deallocation logic to avoid code duplication.
- Thoroughly test your implementation with various scenarios, including cloning, dropping, and attempting to access data after deallocation.