Что такое Arc<T>
2025-02-27 15:58Давайте выясним, чем тип Arc<T> отличается от Rc<T>. Название означает Atomically Reference Counted. Как объясняет нам grok.com, эта штука почти эквивалентна std::shared_ptr из Си++20 и позже. Сгенерённый исходник смотрите в файле: arc.cpp
Question: How Arc<T> would look if implemented in C++?
Let’s dive into how we might implement something like Rust’s Arc<T> in C++. For those unfamiliar, Arc<T> in Rust is a thread-safe, reference-counted smart pointer that allows multiple owners of some data, incrementing a counter when cloned and decrementing it when dropped, freeing the data when the count hits zero. It’s built for concurrent use, so it uses atomic operations to manage the count safely across threads. We’ll aim to replicate that behavior in C++.
In C++, we don’t have a direct equivalent in the standard library, but std::shared_ptr comes close. However, std::shared_ptr isn’t guaranteed to use atomic reference counting for the control block in all implementations (pre-C++20, at least), and its atomic operations were only standardized with std::atomic<std::shared_ptr> in C++20. To truly mirror Arc<T>, we’ll roll our own simplified version using atomic counters, focusing on thread-safety and the core mechanics.
Here’s how it could look:
Question: How Arc<T> would look if implemented in C++?
Let’s dive into how we might implement something like Rust’s Arc<T> in C++. For those unfamiliar, Arc<T> in Rust is a thread-safe, reference-counted smart pointer that allows multiple owners of some data, incrementing a counter when cloned and decrementing it when dropped, freeing the data when the count hits zero. It’s built for concurrent use, so it uses atomic operations to manage the count safely across threads. We’ll aim to replicate that behavior in C++.
In C++, we don’t have a direct equivalent in the standard library, but std::shared_ptr comes close. However, std::shared_ptr isn’t guaranteed to use atomic reference counting for the control block in all implementations (pre-C++20, at least), and its atomic operations were only standardized with std::atomic<std::shared_ptr> in C++20. To truly mirror Arc<T>, we’ll roll our own simplified version using atomic counters, focusing on thread-safety and the core mechanics.
Here’s how it could look:
#include <atomic>
#include <memory>
#include <utility>
#include <type_traits>
// Define Inner as a standalone template struct
template <typename T>
struct ArcInner {
T data;
std::atomic<int> ref_count;
template <typename... Args>
ArcInner(Args&&... args) : data(std::forward<Args>(args)...), ref_count(1) {}
};
template <typename T>
class Arc {
private:
ArcInner<T>* ptr;
void increment_count() {
if (ptr) {
ptr->ref_count.fetch_add(1, std::memory_order_relaxed);
}
}
void decrement_count() {
if (ptr) {
if (ptr->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr;
ptr = nullptr;
}
}
}
public:
// Constructor with constraint: Args must be convertible to T
template <typename... Args, typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
explicit Arc(Args&&... args) : ptr(new ArcInner<T>(std::forward<Args>(args)...)) {}
// Copy constructor
Arc(const Arc& other) : ptr(other.ptr) {
increment_count();
}
// Move constructor
Arc(Arc&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// Assignment operators
Arc& operator=(const Arc& other) {
if (this != &other) {
decrement_count();
ptr = other.ptr;
increment_count();
}
return *this;
}
Arc& operator=(Arc&& other) noexcept {
if (this != &other) {
decrement_count();
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// Destructor
~Arc() {
decrement_count();
}
// Dereference operators
T& operator*() const { return ptr->data; }
T* operator->() const { return &ptr->data; }
// Get the reference count
int use_count() const { return ptr ? ptr->ref_count.load(std::memory_order_relaxed) : 0; }
};
// Helper function to create an Arc
template <typename T, typename... Args>
Arc<T> make_arc(Args&&... args) {
return Arc<T>(std::forward<Args>(args)...);
}
Let’s break this down and compare it to Rust’s Arc<T>:
Key Components
1. Inner Structure:- We define an Inner struct to hold the data (T) and the atomic reference count (std::atomic<int>). This mimics how Arc<T> in Rust bundles the data with its reference counter.
- The constructor uses variadic templates and perfect forwarding to construct T in-place, just like Arc::new.
- increment_count uses fetch_add with memory_order_relaxed since we only need to increment the count, and no other memory synchronization is required at that point.
- decrement_count uses fetch_sub with memory_order_acq_rel to ensure proper synchronization: "acquire" for reading the last value and "release" for ensuring prior writes are visible before deletion. If the count drops to 1 (meaning this was the last reference), we delete the Inner object.
- This matches Arc<T>’s use of atomic operations (like AtomicUsize in Rust) for thread-safety.
- The copy constructor and assignment operator increment the count, ensuring shared ownership.
- The move constructor and move assignment transfer ownership without changing the count, setting the source to nullptr, similar to how Rust handles moves (though Arc is typically cloned, not moved).
- This mirrors Arc’s Clone trait behavior in Rust.
- Decrements the count and cleans up if it’s the last reference, just like Arc’s Drop implementation.
- Dereference operators (* and ->) provide access to the underlying data, akin to Rust’s Deref trait.
Example Usage
#include <iostream>
#include <thread>
// Test function for threading
void thread_func(Arc<int> arc) {
std::cout << "Thread value: " << *arc << ", count: " << arc.use_count() << "\n";
}
int main() {
auto arc = make_arc<int>(42); // Create Arc with int value 42
std::cout << "Main value: " << *arc << ", count: " << arc.use_count() << "\n";
std::thread t(thread_func, arc);
t.join();
std::cout << "After thread, count: " << arc.use_count() << "\n";
return 0;
}
Output (example):
Main value: 42, count: 1
Thread value: 42, count: 2
After thread, count: 1
How It Compares to Arc<T>
- Thread-Safety: Like Arc<T>, this uses atomic operations, making it safe for sharing across threads without data races on the reference count.
- Ownership: Multiple Arcs can own the same data, and it’s freed when the last one is destroyed, just like in Rust.
- Differences:
- Rust’s Arc<T> integrates with the borrow checker, which C++ lacks, so we rely on manual discipline to avoid dangling pointers or misuse.
- Rust provides Arc::try_unwrap to extract the inner value if the count is 1; here, you’d need to add a similar method manually.
- C++’s std::shared_ptr already exists and is more polished (with weak_ptr support), but its pre-C++20 atomic behavior isn’t guaranteed, whereas this explicitly uses atomics like Arc.
Limitations
- No weak references (Rust’s Weak<T> equivalent would need another class).
- Simplistic memory ordering; a production version might need more careful tuning (e.g., memory_order_seq_cst for stricter guarantees).
- No custom allocators or advanced features Rust’s Arc might support internally.

no subject
Date: 2025-02-28 06:03 (UTC)if (ptr) { if (ptr->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) { delete ptr; ptr = nullptr; } }no subject
Date: 2025-02-28 08:11 (UTC)no subject
Date: 2025-02-28 08:24 (UTC)no subject
Date: 2025-02-28 09:04 (UTC)Imagine one thread increments the counter, and another decrements it.
Suspend the decrementing thread right after delete but before ptr is nulled. Now increment is going to mutate some other object that got allocated in place of the wrapper.
Or: suspend increment just before the atomic operation. Now suspend the decrement right after it decrements the counter, and resume the incrementing thread. The incrementing thread will use ptr like it is live, whereas the decrementing thread has destroyed the object.
no subject
Date: 2025-02-28 09:29 (UTC)no subject
Date: 2025-02-28 17:18 (UTC)no subject
Date: 2025-02-28 18:19 (UTC)But I think the trick is in not sharing references to Arc. Does the generated code guarantee that? (In rust that's out of the box)
no subject
Date: 2025-02-28 18:40 (UTC)no subject
Date: 2025-02-28 08:10 (UTC)