☄️Rc<T>

What is the Rc<T> - Reference-counted Smart Pointer?

💡 Rc<T> stands for reference-counted smart pointer that enables multiple ownership of the same data of type T. It provides a way to share ownership between different parts of a program without copying the data itself and keeps track of the number of references to a value, and the value is deallocated only when the reference count drops to zero.

Unlike regular references (&T) which can only have one owner, Rc<T> allows multiple owners to own the same data. This is useful when you need to share data between different parts of your program without transferring ownership and avoid duplication when the data needs to be accessed from multiple locations.

Rc<T> maintains a reference count, which keeps track of how many Rc instances point to the same value. When the last Rc pointing to the value goes out of scope, the value is automatically deallocated, preventing memory leak.

Rc<T> only allows immutable references to the data it wraps. If you need to mutate the data, you should use RefCell<T> in combination with Rc<T> to achieve interior mutability.

Using Box<T> To Share Ownership

In this example, we’re building a Cache for memoizing the data that allows other services to use it for their memoization.

struct Cache {}

struct UserService {
    cache: Cache,
}

struct PostService {
    cache: Cache,
}

fn main() {
    // Create a shared cache
    let cache = Cache {};

    // Create UserService and PostService with the shared cache
    let user_service = UserService { cache: cache };
    let post_service = PostService { cache: cache }; // Error occurs here
}

We define a simple struct named Cache. It doesn't have any fields.

We define two structs UserService and PostService, each containing a single field named cache of type Cache.

In the main function, we create an instance of Cache and assign it to the variable cache.

We initialize an instance of UserService with the cache instance. Here, the ownership of the cache instance is moved to user_service.cache.

In the next line, we initialize an instance of PostService with the cache instance. The code attempts to move the cache instance again into post_service.cache, but cache has already been moved into user_service.cache, and Rust enforces single ownership of values by default, therefore, the error occurs.

The problem here is:

  • Move Semantics: In Rust, when you assign or pass a value to another variable or function, the ownership of that value is transferred (moved). After a move, the original variable can no longer be used.

  • Ownership Rules: Since cache is moved into user_service, it can no longer be used to create post_service. Attempting to use cache after it has been moved results in a compilation error because cache no longer owns the Cache instance.

To share the Cache instance between UserService and PostService, you need to use reference-counted smart pointers (Rc<T>) to allow multiple ownership.

use std::rc::Rc;

#[derive(Debug)]
struct Cache {}

impl Cache {
    fn cache_data(&self) {
        println!("Cache the data")
    }
}

struct UserService {
    cache: Rc<Cache>,
}

struct PostService {
    cache: Rc<Cache>,
}

impl UserService {
    fn cache_data(&self) {
        self.cache.cache_data();
    }
}

impl PostService {
    fn cache_data(&self) {
        self.cache.cache_data();
    }
}

fn main() {
    // Create a shared cache
    let cache = Rc::new(Cache {});

    // Create UserService and PostService with the shared cache
    let user_service = UserService {
        cache: Rc::clone(&cache),
    };
    let post_service = PostService {
        cache: Rc::clone(&cache),
    };

    // Call cache_data function through UserService & PostService
    user_service.cache_data();
    post_service.cache_data();
}

In this code, we wrap the Cache instance inside both the UserService and PostService using Rc smart pointer. This means they share ownership of a single Cache instance.

We also implement a cache_data method that simply returns a string "Cache the data" (presumably a placeholder for actual caching logic).

In the main function, we create a Cache instance and wrap it in Rc::new(Cache {}) to enable reference counting.

Two services, user_service and post_service, are created. They both receive a cloned reference (Rc::clone(&cache)) to the shared cache. By cloning the Rc<Cache>, you create additional references to the same Cache instance. This allows both UserService and PostService to own and use the same Cache instance.

When we clone an Rc, it increments a reference count instead of copying the actual value.

Finally, cache_data methods are called on both services. Since they share the same cache instance, both calls will ultimately execute the cache_data method defined in the Cache struct, potentially printing "Cache the data" twice.

How Rc executes the reference count of the Cache instance:

  1. Initially, the reference count of the Cache instance is 1 when it's wrapped in Rc::new(Cache {}).

  2. When user_service and post_service are created, they both clone the existing Rc<Cache>, incrementing the reference count to 2.

  3. Since both services share the same Rc<Cache>, they are essentially pointing to the same underlying Cache instance.

  4. When main goes out of scope, both user_service and post_service will also eventually go out of scope.

  5. As each Rc<Cache> in these services goes out of scope, the reference count is decremented by 1 (once for each service).

  6. If there are no other owners of the Cache instance, the reference count will finally reach zero, and the Cache instance will be deallocated.

Last updated