vak: (Знайка)
[personal profile] vak
Реализация слабого указателя Weak для Arc отличается от Weak для Rc. Суть та же, но для работы в многопоточной среде используются атомарные счётчики и инкременты-декременты.

Смысл слабого указателя в том, что он не мешает удалить объект. От указателя Arc можно вызвать метод create_weak(), и он вернёт "слабый" указатель типа Weak на тот же объект. Со слабым указателем мало что можно делать, кроме как вызовом метода upgrade() снова получить сильный указатель. Или не получить - если объект уже к этому времени пропал.

Question: Weak for Rc was good. Now please do the same for Weak for Arc. Please show me how it could look in C++.

Let’s implement a version of Rust’s Weak for Arc (Atomic Reference Counting) in C++. In Rust, Arc is similar to Rc but is thread-safe, using atomic operations for reference counting. This allows Arc to be shared across threads, and its companion Weak provides a non-owning reference that can be upgraded to an Arc if the data still exists. The key difference from Rc is that Arc uses atomic counters (std::sync::atomic::AtomicUsize in Rust) instead of plain integers, ensuring safe concurrent access.

In C++, we’ll use std::atomic<size_t> for the counters to mimic this thread-safety. The structure will be similar to our Rc and Weak implementation, but we’ll adjust the reference counting to be atomic. Here’s how it could look: weak-arc.cpp.
#include <iostream>
#include <memory>
#include <optional>
#include <atomic>
#include <thread>

// Forward declaration of Weak
template <typename T>
class Weak;

class ArcControlBlock {
public:
std::atomic<size_t> strong_count;
std::atomic<size_t> weak_count;

ArcControlBlock() : strong_count(1), weak_count(0) {}
};

template <typename T>
class Arc {
private:
T* data;
ArcControlBlock* control;

// Private constructor for internal use
Arc(T* data, ArcControlBlock* control) : data(data), control(control) {}

// Friend declaration to allow Weak<T> to access private constructor
friend class Weak<T>;

public:
// Constructor: Creates a new Arc with data
static Arc<T> new_arc(T value) {
T* data_ptr = new T(std::move(value));
ArcControlBlock* ctrl = new ArcControlBlock();
return Arc(data_ptr, ctrl);
}

// Copy constructor: Atomically increments strong count
Arc(const Arc<T>& other) : data(other.data), control(other.control) {
control->strong_count.fetch_add(1, std::memory_order_relaxed);
}

// Destructor: Atomically decrements strong count and cleans up if needed
~Arc() {
if (control->strong_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete data;
if (control->weak_count.load(std::memory_order_acquire) == 0) {
delete control;
}
}
}

// Access the data (not thread-safe by itself; user must synchronize if needed)
T& operator*() const { return *data; }
T* operator->() const { return data; }

// Getter methods for demonstration
size_t get_strong_count() const { return control->strong_count.load(std::memory_order_relaxed); }
size_t get_weak_count() const { return control->weak_count.load(std::memory_order_relaxed); }

// Declare create_weak
Weak<T> create_weak() const;
};

template <typename T>
class Weak {
private:
T* data;
ArcControlBlock* control;

friend class Arc<T>; // Allow Arc to construct Weak

Weak(T* data, ArcControlBlock* control) : data(data), control(control) {
control->weak_count.fetch_add(1, std::memory_order_relaxed);
}

public:
// Default constructor for invalid Weak
Weak() : data(nullptr), control(nullptr) {}

// Copy constructor: Atomically increments weak count
Weak(const Weak<T>& other) : data(other.data), control(other.control) {
if (control) {
control->weak_count.fetch_add(1, std::memory_order_relaxed);
}
}

// Destructor: Atomically decrements weak count and cleans up if needed
~Weak() {
if (control) {
if (control->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1 &&
control->strong_count.load(std::memory_order_acquire) == 0) {
delete control;
}
}
}

// Attempt to upgrade to an Arc if the data still exists
std::optional<Arc<T>> upgrade() const {
if (control) {
size_t old_count = control->strong_count.load(std::memory_order_acquire);
while (old_count > 0) {
if (control->strong_count.compare_exchange_weak(old_count, old_count + 1,
std::memory_order_acq_rel,
std::memory_order_relaxed)) {
return Arc<T>(data, control);
}
}
}
return std::nullopt;
}
};

// Define create_weak after both classes are fully defined
template <typename T>
Weak<T> Arc<T>::create_weak() const {
return Weak<T>(data, control);
}

Key Differences from Rc/Weak

1. Atomic Counters:
  • ArcControlBlock uses std::atomic<size_t> for strong_count and weak_count.
  • Operations like fetch_add, fetch_sub, and compare_exchange_weak ensure thread-safe updates.
2. Memory Ordering:
  • std::memory_order_relaxed is used for non-critical reads/writes where ordering isn’t needed.
  • std::memory_order_acq_rel (acquire-release) is used for decrements and upgrades to ensure proper synchronization:
    • Acquire ensures we see all prior writes when reading the count.
    • Release ensures our writes are visible before the count drops.
  • upgrade() uses compare_exchange_weak to atomically increment the strong count only if it’s still non-zero, preventing race conditions.
3. Thread-Safety:
  • The reference counting is safe across threads, but accessing *data isn’t inherently thread-safe. In a real application, you’d need to synchronize data access (e.g., with a mutex) if T is modified concurrently.

Notes

Comparison to std::shared_ptr: C++’s std::shared_ptr with std::weak_ptr is similar but uses a control block with atomic counters internally. Our version makes this explicit for clarity.

This implementation captures the essence of Weak for Arc in a thread-safe way.