vak: (Знайка)
[personal profile] vak
Продолжим разбираться с умными указателями в Rust. Глянем на тип Rc<T>. Название расшифровывается как Reference Counted, что намекает. Спросим grok.com. Выходит почти тот же shared_ptr, с двумя отличиями: он не может оказаться NULL, и счетчик простой, а не std::atomic. Сгенерённый ним исходник смотрите в файле: rc.cpp 

Question: How Rc<T> would look if implemented in C++?

Let’s re-implement Rust’s Rc<T> in C++ - a fascinating exercise since Rc<T> introduces reference-counted shared ownership, contrasting with the unique ownership of Box<T>. In Rust, Rc<T> (short for "Reference Counted") allows multiple owners of a heap-allocated T, tracking the number of references via a counter. When the last Rc is dropped, the T is deallocated. This is akin to C++’s std::shared_ptr<T>, but we’ll craft a custom version to mirror Rust’s Rc<T> behavior, focusing on its single-threaded nature (unlike Arc<T> for multi-threading).

What Rc<T> Does in Rust

  • Allocation: Places a T on the heap with a reference count.
  • Shared Ownership: Multiple Rc<T> instances can point to the same T, incrementing the count.
  • Cloning: Rc::clone creates a new reference, bumping the count.
  • Deallocation: When the last Rc is dropped (count hits 0), T is destroyed and memory freed.
  • No Mutability: Rc<T> alone doesn’t allow mutation (use RefCell<T> or Cell<T> for that).
  • Non-Atomic: Single-threaded, unlike Arc<T>.

C++ Implementation

We’ll create an Rc<T> class in C++ that:
  • Uses a control block to store the reference count and T together.
  • Supports copying (to increment the count) and RAII cleanup.
  • Mimics Rust’s immutability by providing const access (mutation would need a separate mechanism).
  • Avoids atomic operations, matching Rc’s single-threaded design.
Here’s the code:
#include <cassert>
#include <utility>
#include <iostream>

// Control block to hold the reference count and the value
template <typename T>
struct RcControlBlock {
size_t count;
T value;

template <typename... Args>
RcControlBlock(Args&&... args) : count(1), value(std::forward<Args>(args)...) {}
};

template <typename T>
class Rc {
private:
RcControlBlock<T>* block;

// Private constructor from raw block (for internal use)
explicit Rc(RcControlBlock<T>* b) : block(b) {}

void increment() {
if (block) {
++block->count;
}
}

void decrement() {
if (block && --block->count == 0) {
delete block;
block = nullptr;
}
}

public:
// Constructor: Allocate and construct T
template <typename... Args>
explicit Rc(Args&&... args) : block(new RcControlBlock<T>(std::forward<Args>(args)...)) {}

// Destructor: Decrease count, cleanup if last reference
~Rc() {
decrement();
}

// Copy constructor: Share ownership
Rc(const Rc& other) : block(other.block) {
increment();
}

// Copy assignment: Share ownership
Rc& operator=(const Rc& other) {
if (this != &other) {
decrement(); // Drop old reference
block = other.block; // Take new reference
increment();
}
return *this;
}

// Move constructor: Transfer pointer, no count change
Rc(Rc&& other) noexcept : block(other.block) {
other.block = nullptr; // Leave moved-from empty
}

// Move assignment: Transfer pointer, no count change
Rc& operator=(Rc&& other) noexcept {
if (this != &other) {
decrement(); // Drop old reference
block = other.block; // Take new reference
other.block = nullptr; // Leave moved-from empty
}
return *this;
}

// Accessors (const only, mimicking Rust's immutability)
const T& operator*() const {
assert(block != nullptr);
return block->value;
}

const T* operator->() const {
assert(block != nullptr);
return &block->value;
}

// Clone method (like Rc::clone)
Rc clone() const {
return Rc(*this); // Uses copy constructor
}

// Get reference count (for debugging)
size_t use_count() const {
return block ? block->count : 0;
}
};

// Factory function (like Rc::new)
template <typename T, typename... Args>
Rc<T> make_rc(Args&&... args) {
return Rc<T>(std::forward<Args>(args)...);
}
This Rc<T> captures Rust’s essence - shared, immutable, single-threaded ownership.

Breaking It Down

1. Control Block
  • Rust: Rc<T> internally uses a struct like RcBox { ref_count: NonZeroUsize, value: T }.
  • C++: RcControlBlock<T> holds count and value. Allocated once, shared by all Rc instances.
  • Why: Keeps the count and data together, ensuring atomic-like management (though non-atomic here).
2. Allocation
  • Rust: Rc::new allocates via the global allocator, initializes T, sets count to 1.
  • C++: Constructor uses new RcControlBlock<T>(args...) with perfect forwarding.
  • Why: C++’s new handles both allocation and construction, like Rust.
3. Shared Ownership
  • Rust: Cloning via Rc::clone increments the count.
  • C++: Copy constructor and clone() call increment(), sharing the block.
  • Why: Mimics Rust’s shared ownership without C++’s atomic ops (unlike shared_ptr).
4. Deallocation
  • Rust: Drop decrements the count; if 0, deallocates.
  • C++: Destructor calls decrement(); if count hits 0, deletes the block.
  • Why: RAII ensures cleanup, matching Rust’s scope-based freeing.
5. Move Semantics
  • Rust: Moves are cheap pointer transfers.
  • C++: Move constructor/assignment transfers block, nulls the source, no count change.
  • Why: Optimizes transfer without affecting reference count.
6. Access
  • Rust: Deref to &T (immutable).
  • C++: Const operator* and operator-> enforce immutability.
  • Why: Reflects Rc<T>’s read-only nature (mutation needs RefCell).

Differences from std::shared_ptr<T>

1. Thread Safety
  • Rc<T>: Non-atomic, single-threaded (uses usize for count).
  • std::shared_ptr<T>: Atomic ref counting, thread-safe (uses std::atomic).
  • Our Rc<T>: Non-atomic, like Rust’s Rc, cheaper but unsafe across threads.
2. Deleter
  • Rc<T>: Fixed deallocation (global allocator).
  • std::shared_ptr<T>: Custom deleters via constructor.
  • Our Rc<T>: Fixed delete, no customization.
3. Weak Pointers
  • Rc<T>: Pairs with Weak<T> for non-owning refs (separate count).
  • std::shared_ptr<T>: Pairs with std::weak_ptr<T> (separate weak count).
  • Our Rc<T>: No Weak equivalent; we’d need another class and a weak count.
4. Allocation
  • Rc<T>: Single allocation for count and T.
  • std::shared_ptr<T>: Often two allocations (control block separate from T), unless make_shared is used.
  • Our Rc<T>: Single allocation in RcControlBlock, like Rust.
5. Nullability
  • Rc<T>: Always points to a valid T.
  • std::shared_ptr<T>: Can be null.
  • Our Rc<T>: Non-null by design, like Rust.
6. Overhead
  • Rc<T>: sizeof(*mut T) (pointer size).
  • std::shared_ptr<T>: Often larger (pointer + control block pointer or atomic overhead).
  • Our Rc<T>: sizeof(T*), minimal like Rust.

Rust Internals Peek

Rust’s Rc<T> is roughly:
struct RcBox<T: ?Sized> {
strong: usize, // Reference count
value: T,
}

pub struct Rc<T: ?Sized> {
ptr: *mut RcBox<T>,
}

impl<T> Rc<T> {
pub fn new(value: T) -> Rc<T> {
let ptr = unsafe { alloc::alloc(Layout::for_value(&RcBox { strong: 1, value })) };
// Initialize...
Rc { ptr }
}
}

impl<T: ?Sized> Clone for Rc<T> {
fn clone(&self) -> Rc<T> {
unsafe { (*self.ptr).strong += 1; }
Rc { ptr: self.ptr }
}
}

impl<T: ?Sized> Drop for Rc<T> {
fn drop(&mut self) {
unsafe {
(*self.ptr).strong -= 1;
if (*self.ptr).strong == 0 {
alloc::dealloc(self.ptr as *mut u8, Layout::for_value(&*self.ptr));
}
}
}
}
Our C++ version mirrors this single-allocation, non-atomic design.
This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org