Program Listing for File ref_tracker.hpp

Return to documentation for file (include/wrench/memory/ref_tracker.hpp)

//==--- wrench/memory/ref_tracker.hpp ---------------------- -*- C++ -*- ---==//
//
//                                Wrench
//
//                      Copyright (c) 2020 Rob Clucas
//
//  This file is distributed under the MIT License. See LICENSE for details.
//
//==------------------------------------------------------------------------==//
//
//
//==------------------------------------------------------------------------==/

#ifndef WRENCH_MEMORY_REF_TRACKER_HPP
#define WRENCH_MEMORY_REF_TRACKER_HPP

#include <atomic>
#include <type_traits>
#include <utility>

namespace wrench {

//==--- [forward declarations & aliases] -----------------------------------==//

template <typename Impl>
class RefTracker;

class SingleThreadedRefTracker;

class MultiThreadedRefTracker;

using DefaultRefTracker =
#if defined(WRENCH_SINGLE_THREADED)
  SingleThreadedRefTracker;
#else
  MultiThreadedRefTracker;
#endif

template <typename T>
static constexpr bool is_ref_tracker_v =
  std::is_base_of_v<RefTracker<std::decay_t<T>>, std::decay_t<T>>;

//==--- [implementation] ---------------------------------------------------==//

template <typename Impl>
class RefTracker {
  wrench_no_discard auto impl() noexcept -> Impl* {
    return static_cast<Impl*>(this);
  }

  wrench_no_discard auto impl() const noexcept -> const Impl* {
    return static_cast<const Impl*>(this);
  }

 public:
  auto add_reference() noexcept -> void {
    impl()->add_reference_impl();
  }

  auto release() noexcept -> bool {
    return impl()->release_impl();
  }

  template <typename T, typename Deleter>
  auto destroy_resource(T* resource, Deleter&& deleter) noexcept -> void {
    impl()->destroy_resource_impl(resource, std::forward<Deleter>(deleter));
  }
};

//==--- [single-threaded implementation] -----------------------------------==//

class SingleThreadedRefTracker : public RefTracker<SingleThreadedRefTracker> {
 public:
  using Counter = size_t;

  auto add_reference_impl() noexcept -> void {
    ref_count_++;
  }

  auto release_impl() noexcept -> bool {
    return --ref_count_ == 0;
  }

  template <typename T, typename Deleter>
  auto destroy_resource_impl(T* resource, Deleter&& deleter) noexcept -> void {
    deleter(resource);
  }

 private:
  Counter ref_count_ = 1;
};

//==---[multi-threaded implementation] -------------------------------------==//

class MultiThreadedRefTracker : public RefTracker<MultiThreadedRefTracker> {
 public:
  using Counter = std::atomic_size_t;

  MultiThreadedRefTracker() noexcept {
    ref_count_.store(1, std::memory_order_relaxed);
  }

  //==--- [move] -----------------------------------------------------------==//

  auto add_reference_impl() noexcept -> void {
    // Memory order relaxed because new references can only be created from
    // existing instances with the reference count, so we just care about
    // incrementing the ref atomically, not about the memory ordering here.
    ref_count_.fetch_add(1, std::memory_order_relaxed);
  }

  auto release() noexcept -> bool {
    // Here we need to ensure that any access from another thread __happens
    // before__ the deleting the object, though a call to `destroy` __if__ this
    // returns true.
    //
    // To ensure this, no reads/or write can be reordered to be after the
    // `fetch_sub` (i.e they happen before). Another thread might hold the last
    // reference, and before deleting, the `fetch_sub` needs to happen on
    // __this__ thread __before__ that thread deletes, which is done with
    // `memory_order_release`.
    //
    // Note the delete needs a `memory_order_acquire` before it, so that there
    // is a valid release-acquire sequence. We could use
    // memory_order_acq_release` here, but that wastes an aquire for each
    // decrement, when it's only required before deleting. We put the thread
    // fence in the destruction implementation to ensure the correct behaviour.
    return ref_count_.fetch_sub(1, std::memory_order_release) == 1;
  }

  template <typename T, typename Deleter>
  auto destroy_resource_impl(T* resource, Deleter&& deleter) noexcept -> void {
    // Here we need to ensure that no read or write is ordered before the
    // `fetch_sub` in the `release` call. Otherwise another thread might could
    // see a destroyed object before the reference count is zero. This is done
    // with the barrier with `memory_order_acquire`.
    std::atomic_thread_fence(std::memory_order_acquire);
    deleter(resource);
  }

 private:
  Counter ref_count_ = 1;
};

} // namespace wrench

#endif // WRENCH_MEMORY_REF_TRACKER_HPP