Тип RefCell<T>
2025-03-01 16:08![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Тип RefCell<T>, аналогично Cell<T>, тоже контейнер на одно значение произвольного типа. Но к самому значению нельзя достучаться прямо, а надо сначала получить ссылку на него методами borrow() или borrow_mut().
Метод borrow() даёт константную ссылку. Значение можно получить методом get(), но нельзя изменить. Таких константных ссылок можно насоздавать сколько угодно - при условии, что не существует мутабельной ссылки.
Метод borrow_mut() даёт мутабельную ссылку. Значение можно изменить методом set(). Но мутабельную ссылку можно получить только одну-единственную, и при отсутствии константных ссылок.
Question: Please show me implementation of type RefCell<T> in C++.
Let’s tackle implementing Rust’s RefCell<T> in C++. This is an exciting challenge because RefCell<T> takes interior mutability a step further than Cell<T>. While Cell<T> allows mutation through a shared reference with a simple copy or swap, RefCell<T> provides borrowed access—either immutable (shared) or mutable (exclusive)—with runtime checks to enforce Rust’s borrowing rules: you can have many immutable borrows or one mutable borrow, but never both at once. In Rust, this is all about safety without locks, and we’ll aim to replicate that dynamic enforcement in C++.
In C++, we don’t have a borrow checker, so we’ll use a runtime mechanism to track borrows, similar to how RefCell uses a counter in Rust. The implementation will involve:
Метод borrow() даёт константную ссылку. Значение можно получить методом get(), но нельзя изменить. Таких константных ссылок можно насоздавать сколько угодно - при условии, что не существует мутабельной ссылки.
Метод borrow_mut() даёт мутабельную ссылку. Значение можно изменить методом set(). Но мутабельную ссылку можно получить только одну-единственную, и при отсутствии константных ссылок.
Question: Please show me implementation of type RefCell<T> in C++.
Let’s tackle implementing Rust’s RefCell<T> in C++. This is an exciting challenge because RefCell<T> takes interior mutability a step further than Cell<T>. While Cell<T> allows mutation through a shared reference with a simple copy or swap, RefCell<T> provides borrowed access—either immutable (shared) or mutable (exclusive)—with runtime checks to enforce Rust’s borrowing rules: you can have many immutable borrows or one mutable borrow, but never both at once. In Rust, this is all about safety without locks, and we’ll aim to replicate that dynamic enforcement in C++.
In C++, we don’t have a borrow checker, so we’ll use a runtime mechanism to track borrows, similar to how RefCell uses a counter in Rust. The implementation will involve:
- A container for the value.
- Counters for active immutable and mutable borrows.
- Methods to borrow the value immutably (borrow) or mutably (borrow_mut), returning smart pointers that enforce the borrowing rules.
- Runtime checks to panic (or throw, in C++ terms) if the rules are violated.
#include <iostream>
#include <memory>
#include <stdexcept>
#include <utility>
template <typename T>
class RefCell {
private:
T value;
mutable size_t borrow_count; // Tracks immutable borrows
mutable bool borrowed_mut; // Tracks if there's a mutable borrow
public:
explicit RefCell(T initial) : value(std::move(initial)), borrow_count(0), borrowed_mut(false) {}
class Ref {
private:
const RefCell<T>* cell;
friend class RefCell<T>;
Ref(const RefCell<T>* c) : cell(c) {
if (cell->borrowed_mut) {
throw std::runtime_error("Already mutably borrowed");
}
cell->borrow_count++;
}
public:
~Ref() {
if (cell) {
cell->borrow_count--;
}
}
Ref(const Ref&) = delete;
Ref& operator=(const Ref&) = delete;
Ref(Ref&& other) noexcept : cell(other.cell) { other.cell = nullptr; }
const T& get() const { return cell->value; }
};
class RefMut {
private:
RefCell<T>* cell;
friend class RefCell<T>;
RefMut(RefCell<T>* c) : cell(c) {
if (c->borrow_count > 0 || c->borrowed_mut) {
throw std::runtime_error("Already borrowed");
}
c->borrowed_mut = true;
}
public:
~RefMut() {
if (cell) {
cell->borrowed_mut = false;
}
}
RefMut(const RefMut&) = delete;
RefMut& operator=(const RefMut&) = delete;
RefMut(RefMut&& other) noexcept : cell(other.cell) { other.cell = nullptr; }
T& get() { return cell->value; }
void set(T new_value) {
cell->value = std::move(new_value);
}
};
Ref borrow() const { return Ref(this); }
RefMut borrow_mut() { return RefMut(this); }
};
How It Works
1. State Tracking:- borrow_count: A mutable counter for active immutable borrows.
- borrowed_mut: A mutable flag indicating an active mutable borrow. Both are mutable so borrowing works even on a const RefCell.
- Ref: Represents an immutable borrow. It increments borrow_count on creation and decrements it on destruction. Throws if a mutable borrow exists.
- RefMut: Represents a mutable borrow. Sets borrowed_mut to true on creation and false on destruction. Throws if any borrows (immutable or mutable) exist.
- The Ref and RefMut objects use RAII to automatically manage the borrow state. When they go out of scope, the counters or flags are updated, ensuring no dangling borrows.
- borrow(): Succeeds if no mutable borrow exists, allowing multiple immutable borrows.
- borrow_mut(): Succeeds only if no borrows (immutable or mutable) are active, enforcing exclusivity.
- Throws std::runtime_error when borrowing rules are violated, mimicking Rust’s panic!.
Comparison to Rust’s RefCell<T>
- Runtime Checks: Like Rust, this enforces borrowing rules at runtime. Rust returns Ref or RefMut smart pointers (via Result), while here we use exceptions for simplicity. You could use std::optional or a custom Result type to avoid exceptions if preferred.
- Interior Mutability: The mutable keyword ensures we can borrow and modify even through a const RefCell, matching Rust’s behavior.
- Thread Safety: Like Rust’s RefCell, this isn’t thread-safe. Use mutexes or atomics for concurrency.
Notes
- Move Semantics: The borrow types support moving but not copying to prevent double-counting borrows.
- Const Correctness: Methods are const to allow borrowing from a const RefCell, with mutable enabling the internal state changes.
- Limitations: If T isn’t copyable, Ref::get() returning const T by value won’t work. You could modify it to return const T& instead.
- Alignment with Rust: In Rust, you’d write cell.borrow_mut().deref_mut() to get a &mut T and then assign to it. Here, RefMut::set() provides a cleaner API while still requiring a mutable borrow first, preserving the runtime safety of RefCell.
- Safety: Since RefMut can only be created when no other borrows exist (checked in its constructor), set() is inherently safe without additional checks.