aboutsummaryrefslogtreecommitdiff
path: root/owning_mutex.h
blob: 82756da0dfcc9314d54dc8e16f57ea1fa5def761 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#ifndef UTILS_MUTEX_H
#define UTILS_MUTEX_H

#include <mutex>

#include <cassert>

template <typename T, typename M>
struct guard;

// -- OWNING MUTEX -------------------------------------------------------------

/// owning_mutex
///
/// An mutex wrapper type that owns a value of type T and provides mutual
/// exclusive access to that value through guard objects. Guard objects are
/// obtained from the wrapper API. When a guard goes out of scope, the mutex
/// will be unlocked automatically.
///
/// The mutex type can be controlled by the template type argument M.
///   M: BasicLockable
///
/// EXAMPLE:
///   struct data { int a; };
///   owning_mutex<data> val{1};
///
///   {
///     auto guard = val.lock();
///     guard->a = 1337;
///     // mutex will be unlocked after this scope
///   }
template <typename T, typename M = std::mutex>
struct owning_mutex {
  template <typename... Args>
  constexpr explicit owning_mutex(Args... args)
      : m_val{std::forward<Args>(args)...} {}

  owning_mutex(const owning_mutex&) = delete;
  owning_mutex(owning_mutex&&) = delete;

  guard<T, M> lock() {
    return {m_mtx, m_val};
  }

 private:
  M m_mtx;
  T m_val;
};

// -- GUARD --------------------------------------------------------------------

#if __cplusplus >= 201703L

template <typename T, typename M>
struct [[nodiscard]] guard {
  guard(M& mtx, T& val) : m_lk{mtx}, m_val{val} {}

  // With the guaranteed copy elision (cpp17) we can truly delete the
  // copy/move constructor of the guard type.
  //
  // https://stackoverflow.com/a/38043447
  guard(const guard&) = delete;
  guard(guard&&) = delete;

  T& operator*() {
    return m_val;
  }

  T* operator->() {
    return &m_val;
  }

 private:
  std::lock_guard<M> m_lk;
  T& m_val;
};

#else  // before cpp17

template <typename T, typename M>
struct guard {
  guard(M& mtx, T& val) : m_mtx{&mtx}, m_val{val} {
    m_mtx->lock();
  }

  ~guard() {
    if (m_mtx) {
      m_mtx->unlock();
    }
  }

  T& operator*() {
    assert(m_mtx != nullptr);
    return m_val;
  }

  T* operator->() {
    if (!m_mtx) {
      return nullptr;
    }
    return &m_val;
  }

  guard(const guard&) = delete;
  // Implement move constructor for cases where the compiler does no copy
  // elision.
  // For API compatibility with the cpp17 version, the move constructor
  // should not be explicitly invoked by the user.
  //
  // SAFETY: Exclusive access to T is guaranteed as at any given time only a
  // single *guard* instance is NOT moved.
  //
  // UB: The *guard* must not be moved across thread boundaries and dropped
  // there.
  guard(guard&& rhs) noexcept : m_mtx{rhs.m_mtx}, m_val{rhs.m_val} {
    rhs.m_mtx = nullptr;
  }

 private:
  M* m_mtx;
  T& m_val;
};
#endif

#endif