mercredi 14 octobre 2020

Is it safe to use relaxed memory ordering on the reference counter of a shared pointer class?

I have implemented a custom shared pointer class. It uses a pointer to an atomic int as a reference counter, and relaxed memory ordering for increments/decrements to the counter.

I'm wondering if:

  1. This still guarantees that the deletion of the associated object (of type T) will only happen once, after fetch_sub returns 1 for the previous value.
  2. Could reordering of operations by the processor somehow lead to the deletion of the object while other shared pointers referencing the same object are still in use?

Here is the code, note the calls to fetch_add and fetch_sub with std::memory_order_relaxed:

template <typename T> class SharedPtr {
public:
  SharedPtr(T *obj) : allocated_(obj), ref_count_(new std::atomic<int>(1)) {}

  ~SharedPtr() {
    if (ref_count_ && ref_count_->fetch_sub(1, std::memory_order_relaxed) == 1) {
      delete allocated_;
      delete ref_count_;
    }
  }

  SharedPtr(const SharedPtr &copy_construct) {
    allocated_ = copy_construct.allocated_;
    ref_count_ = copy_construct.ref_count_;
    if (ref_count_) {
      ref_count_->fetch_add(1, std::memory_order_relaxed);
    }
  }

  SharedPtr& operator=(const SharedPtr &copy_assign) {
    if (this != &copy_assign) {
      if (ref_count_ && ref_count_->fetch_sub(1, std::memory_order_relaxed) == 1) {
        delete allocated_;
        delete ref_count_;
      }
      ref_count_ = copy_assign.ref_count_;
      ref_count_->fetch_add(1, std::memory_order_relaxed);
      allocated_ = copy_assign.allocated_;
    }
    return *this;
  }

  SharedPtr(SharedPtr&& move_construct) {
    ref_count_ =  move_construct.ref_count_;  
    allocated_ =  move_construct.allocated_; 
    move_construct.ref_count_ = nullptr;
    move_construct.allocated_ = nullptr;
  }

  SharedPtr& operator=(SharedPtr&& move_assign) {
    if (this != &move_assign) {
      if (ref_count_ && ref_count_->fetch_sub(1, std::memory_order_relaxed) == 1) {
        delete allocated_;
        delete ref_count_;
      }
      ref_count_ =  move_assign.ref_count_;  
      allocated_ =  move_assign.allocated_; 
      move_assign.ref_count_ = nullptr;
      move_assign.allocated_ = nullptr;
    }
    return *this;
  }

  T *Get() { return allocated_; }

private:
  T *allocated_ = nullptr;
  std::atomic<int> *ref_count_ = nullptr;
};

Aucun commentaire:

Enregistrer un commentaire