As you may know, C++20 has added std::atomic<std::shared_ptr<T>>
specialization to the standard, but sadly, most compilers have not implemented it yet. So I decided to implement it myself.
I want to know if I can improve this code or not. In addition, I'm not sure if my implementations of wait()
, notify_one()
and notify_all()
are correct using condition variable.
#include <atomic>
#include <memory>
#include <condition_variable>
#include <thread>
#include <version>
#if !defined(__cpp_lib_atomic_shared_ptr) || (__cpp_lib_atomic_shared_ptr == 0)
namespace std {
template<typename T>
struct atomic<shared_ptr<T>> {
using value_type = shared_ptr<T>;
static constexpr bool is_always_lock_free = false;
bool is_lock_free() const noexcept {
return false;
}
constexpr atomic() noexcept {};
atomic(shared_ptr<T> desired) noexcept : ptr_(std::move(desired)) {};
atomic(const atomic&) = delete;
void operator=(const atomic&) = delete;
void store(shared_ptr<T> desired, memory_order order = memory_order::seq_cst) noexcept {
std::lock_guard<std::mutex> lk(cv_m_);
atomic_store_explicit(&ptr_, std::move(desired), order);
}
void operator=(shared_ptr<T> desired) noexcept {
store(std::move(desired));
}
shared_ptr<T> load(memory_order order = memory_order::seq_cst) const noexcept {
return atomic_load_explicit(&ptr_, order);
}
operator shared_ptr<T>() const noexcept {
return load();
}
shared_ptr<T> exchange(shared_ptr<T> desired, memory_order order = memory_order::seq_cst) noexcept {
return atomic_exchange_explicit(&ptr_, std::move(desired), order);
}
bool compare_exchange_weak(shared_ptr<T>& expected, shared_ptr<T> desired,
memory_order success, memory_order failure) noexcept {
return atomic_compare_exchange_weak_explicit(&ptr_, &expected, std::move(desired), success, failure);
}
bool compare_exchange_strong(shared_ptr<T>& expected, shared_ptr<T> desired,
memory_order success, memory_order failure) noexcept {
return atomic_compare_exchange_strong_explicit(&ptr_, &expected, std::move(desired), success, failure);
}
bool compare_exchange_weak(shared_ptr<T>& expected, shared_ptr<T> desired,
memory_order order = memory_order::seq_cst) noexcept {
return compare_exchange_weak(expected, std::move(desired), order, convert_order(order));
}
bool compare_exchange_strong(shared_ptr<T>& expected, shared_ptr<T> desired,
memory_order order = memory_order::seq_cst) noexcept {
return compare_exchange_strong(expected, std::move(desired), order, convert_order(order));
}
void wait(shared_ptr<T> old, memory_order order = memory_order::seq_cst) const noexcept {
std::unique_lock<std::mutex> lk(cv_m_);
cv_.wait(lk, [&]{ return !(load(order) == old); });
}
void notify_one() noexcept {
cv_.notify_one();
}
void notify_all() noexcept {
cv_.notify_all();
}
private:
shared_ptr<T> ptr_;
mutable std::condition_variable cv_;
mutable std::mutex cv_m_;
constexpr memory_order convert_order(memory_order order) {
switch(order) {
case std::memory_order_acq_rel:
return std::memory_order_acquire;
case std::memory_order_release:
return std::memory_order_relaxed;
default:
return order;
}
}
};
}
#endif
int main() {
std::atomic<std::shared_ptr<int>> a;
}
I will be happy about your suggestions. Thanks in advance.
1 Answer 1
Simple things:
operator=
(削除) should (削除ここまで)would normally return*this
rather thanvoid
:atomic& operator=(value_type desired) noexcept { store(std::move(desired)); return *this; }
But it seems that the standard actually requires
void
return here, so the implementation is correct.Default construction should initialize members:
private: value_type ptr_= {}; mutable std::condition_variable cv_ = {}; mutable std::mutex cv_m_= {};
Since we're writing for C++20, we can take advantage of class template argument deduction to simplify, e.g.
std::lock_guard lk{cv_m_};
I don't like the name of convert_order()
. I might call it something like order_without_release()
, to be more specific about why it's used. Yes, it's a private function, but it's still important to keep it clear.
Similarly, I'd name the success
and failure
arguments more explicitly - perhaps success_mem_order
etc.?
I think that convert_order
ought to be constexpr static
, since it doesn't use *this
.
Our implementation is not lock-free:
static constexpr bool is_always_lock_free = false; bool is_lock_free() const noexcept { return false; }
A library implementation with access to internals of shared-pointer might be able to avoid that, but we can't. Still, it's better than no implementation at all.
Otherwise, this looks pretty good. We should be able to use a very similar implementation to provide std::shared_ptr<std::weak_ptr<T>>
, too.
-
\$\begingroup\$ I really don't know why I have messed up first 2 points.... <.< \$\endgroup\$Afshin– Afshin2022年01月11日 11:24:47 +00:00Commented Jan 11, 2022 at 11:24
-
\$\begingroup\$ I just remembered why my
operator=
returnsvoid
. because I copied template of this class from standard: eel.is/c++draft/util.smartptr.atomic.shared in addition, in cpp ref it also returnsvoid
. en.cppreference.com/w/cpp/memory/shared_ptr/atomic2 \$\endgroup\$Afshin– Afshin2022年01月11日 11:32:25 +00:00Commented Jan 11, 2022 at 11:32 -
\$\begingroup\$ Ah, interesting. And the non-specialised
std::atomic
returns a copy of the stored value. I'm not sure why. \$\endgroup\$Toby Speight– Toby Speight2022年01月11日 11:43:42 +00:00Commented Jan 11, 2022 at 11:43 -
1\$\begingroup\$ @TobySpeight, my expectation would be that the value returned from = is exactly the same used for assignment itself. Since atomics can change between any atomic operations, I guess returning a copy would lead to less bugs that are extremely hard to reproduce. \$\endgroup\$Incomputable– Incomputable2022年01月11日 12:45:12 +00:00Commented Jan 11, 2022 at 12:45
-
1\$\begingroup\$ I think because if
operator=
for this specialization returns a copy, internalshared_ptr
's reference count should be increased which is not logical. Then they probably decided to usevoid
returning function. \$\endgroup\$Afshin– Afshin2022年01月11日 12:58:36 +00:00Commented Jan 11, 2022 at 12:58