6
\$\begingroup\$

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.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Simple things:

  • operator= should would normally return *this rather than void:

    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.

\$\endgroup\$
5
  • \$\begingroup\$ I really don't know why I have messed up first 2 points.... <.< \$\endgroup\$
    – Afshin
    Jan 11, 2022 at 11:24
  • \$\begingroup\$ I just remembered why my operator= returns void. because I copied template of this class from standard: eel.is/c++draft/util.smartptr.atomic.shared in addition, in cpp ref it also returns void. en.cppreference.com/w/cpp/memory/shared_ptr/atomic2 \$\endgroup\$
    – Afshin
    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\$ 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\$ Jan 11, 2022 at 12:45
  • 1
    \$\begingroup\$ I think because if operator= for this specialization returns a copy, internal shared_ptr's reference count should be increased which is not logical. Then they probably decided to use void returning function. \$\endgroup\$
    – Afshin
    Jan 11, 2022 at 12:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.