NEURON
data_handle.hpp
Go to the documentation of this file.
1 #pragma once
2 #include "backtrace_utils.h"
5 
6 #include <ostream>
7 #include <sstream>
8 
9 namespace neuron::container {
10 struct do_not_search_t {};
11 inline constexpr do_not_search_t do_not_search{};
12 
13 namespace detail {
14 // 3rd argument ensures the no-op implementation has lower precedence than the one that prints val.
15 // This implementation avoids passing a T through ... in the no-output-operator case, which is
16 // important if T is a non-trivial type without an output operator.
17 template <typename T>
18 std::ostream& print_value_impl(std::ostream& os, T const&, ...) {
19  return os;
20 }
21 template <typename T>
22 auto print_value_impl(std::ostream& os, T const& val, std::nullptr_t) -> decltype(os << val) {
23  return os << " val=" << val;
24 }
25 template <typename T>
26 std::ostream& print_value(std::ostream& os, T const& val) {
27  return print_value_impl(os, val, nullptr);
28 }
29 } // namespace detail
30 
31 /** @brief Stable handle to a generic value.
32  *
33  * Without this type one can already hold a Node::handle `foo` and call
34  * something like `foo.v()` to get that Node's voltage in a way that is stable
35  * against permutations of the underlying data. The data_handle concept is
36  * intended to be used if we want to erase the detail that the quantity is a
37  * voltage, or that it belongs to a Node, and simply treat it as a
38  * floating-point value that we may want to dereference later -- essentially a
39  * substitute for double*.
40  *
41  * Implementation: like Node::handle we can store a std::size_t* that we
42  * dereference to find out either:
43  * - the current offset in the underlying container, or
44  * - that the object being referred to (e.g. a Node) no longer exists
45  *
46  * Assuming nothing has been invalidated, we have an offset, a type, and the
47  * fundamental assumption behind all of this that the underlying data are
48  * contiguous -- we "just" need to know the address of the start of the
49  * underlying storage vector without adding specific type information (like
50  * Node) to this class. The simplest way of doing this is to assume that the
51  * underlying storage is always std::vector<T> (or a custom allocator that is
52  * always the same type in neuron::container::*). Note that storing T* or
53  * span<T> would not work if the underlying storage is reallocated.
54  *
55  * @todo Const correctness -- data_handle should be like span:
56  * data_handle<double> can read + write the value, data_handle<double const>
57  * can only read the value. const applied to the data_handle itself should just
58  * control whether or not it can be rebound to refer elsewhere.
59  */
60 template <typename T>
61 struct data_handle {
62  data_handle() = default;
63 
64  /** @brief Construct a data_handle from a plain pointer.
65  */
66  explicit data_handle(T* raw_ptr) {
67  // Null pointer -> null handle.
68  if (!raw_ptr) {
69  return;
70  }
71  // First see if we can find a neuron::container that contains the current
72  // value of `raw_ptr` and promote it into a container/handle pair. This is
73  // ugly and inefficient; you should prefer using the other constructor.
74  auto needle = utils::find_data_handle(raw_ptr);
75  if (needle) {
76  *this = std::move(needle);
77  } else {
78  // If that didn't work, just save the plain pointer value. This is unsafe
79  // and should be removed. It is purely meant as an intermediate step, if
80  // you use it then the guarantees above will be broken.
82  }
83  }
84 
85  /**
86  * @brief Create a data_handle<T> wrapping the raw T*
87  *
88  * Unlike the constructor taking T*, this does *not* attempt to promote raw pointers to modern
89  * data_handles.
90  */
93 
94  /**
95  * @brief Get a data handle to a different element of the same array variable.
96  *
97  * Given an array variable a[N], this method allows a handle to a[i] to yield a handle to a[j]
98  * within the same logical row. If the handle is wrapping a raw pointer T*, the shift is applied
99  * to that raw pointer.
100  */
101  [[nodiscard]] data_handle next_array_element(int shift = 1) const {
103  int const new_array_index{m_array_index + shift};
104  if (new_array_index < 0 || new_array_index >= m_array_dim) {
105  std::ostringstream oss;
106  oss << *this << " next_array_element(" << shift << "): out of range";
107  throw std::runtime_error(oss.str());
108  }
109  return {m_offset,
110  static_cast<T* const*>(m_container_or_raw_ptr),
111  m_array_dim,
112  new_array_index};
113  } else {
114  return {do_not_search, static_cast<T*>(m_container_or_raw_ptr) + shift};
115  }
116  }
117 
118  /**
119  * @brief Query whether this data handle is in "modern" mode.
120  * @return true if the handle was created as a permutation-stable handle to an soa<...> data
121  * structure, otherwise false.
122  *
123  * Note that this does *not* mean that the handle is still valid. The referred-to row and/or
124  * column may have gone away in the meantime.
125  */
126  [[nodiscard]] bool refers_to_a_modern_data_structure() const {
127  return bool{m_offset} || m_offset.was_once_valid();
128  }
129 
130  // TODO a const-ness cleanup. It should be possible to get
131  // data_handle<T> from a view into a frozen container, even though it
132  // isn't possible to get std::vector<T>& from a frozen container. And
133  // data_handle<T const> should forbid writing to the data value.
135  T* const* container,
136  int array_dim,
137  int array_index)
138  : m_offset{std::move(offset)}
139  , m_container_or_raw_ptr{const_cast<T**>(container)}
140  , m_array_dim{array_dim}
141  , m_array_index{array_index} {}
142 
143  [[nodiscard]] explicit operator bool() const {
144  if (bool{m_offset}) {
145  // valid, modern identifier (i.e. row is valid)
146  return container_data(); // also check if the column is valid
147  } else if (m_offset.was_once_valid()) {
148  // once-valid, modern. no longer valid
149  return false;
150  } else {
151  // null or raw pointer
152  return m_container_or_raw_ptr;
153  }
154  }
155 
156  /** Query whether this generic handle points to a value from the `Tag` field
157  * of the given container.
158  */
159  template <typename Tag, typename Container>
160  [[nodiscard]] bool refers_to(Container const& container) const {
161  static_assert(Container::template has_tag_v<Tag>);
162  if (bool{m_offset} || m_offset.was_once_valid()) {
163  // basically in modern mode (possibly the entry we refer to has
164  // died)
165  return container.template is_storage_pointer<Tag>(container_data());
166  } else {
167  // raw-ptr mode or null
168  return false;
169  }
170  }
171 
172  /**
173  * @brief Get the current logical row number.
174  */
175  [[nodiscard]] std::size_t current_row() const {
177  assert(m_offset);
178  return m_offset.current_row();
179  }
180 
181  private:
182  // Try and cover the different operator* and operator T* cases with/without
183  // const in a more composable way
184  [[nodiscard]] T* raw_ptr() {
185  return static_cast<T*>(m_container_or_raw_ptr);
186  }
187  [[nodiscard]] T const* raw_ptr() const {
188  return static_cast<T const*>(m_container_or_raw_ptr);
189  }
190  [[nodiscard]] T* container_data() {
191  return *static_cast<T* const*>(m_container_or_raw_ptr);
192  }
193  [[nodiscard]] T const* container_data() const {
194  return *static_cast<T const* const*>(m_container_or_raw_ptr);
195  }
196  template <typename This>
197  [[nodiscard]] static auto get_ptr_helper(This& this_ref) {
198  if (this_ref.m_offset.has_always_been_null()) {
199  // null or raw pointer
200  return this_ref.raw_ptr();
201  }
202  if (this_ref.m_offset) {
203  // valid, modern mode *identifier*; i.e. we know what offset into a vector we're
204  // supposed to be looking at. It's also possible that the vector doesn't exist anymore,
205  // either because the whole soa<...> container was deleted, or because an optional field
206  // was toggled off in an existing soa<...> container. In that case, the base pointer
207  // will be null.
208  if (auto* const base_ptr = this_ref.container_data(); base_ptr) {
209  // the array still exists
210  return base_ptr + this_ref.m_array_dim * this_ref.m_offset.current_row() +
211  this_ref.m_array_index;
212  }
213  // the vector doesn't exist anymore => return nullptr
214  return decltype(this_ref.raw_ptr()){nullptr};
215  }
216  // no longer valid, modern mode
217  return decltype(this_ref.raw_ptr()){nullptr};
218  }
219 
220  public:
221  [[nodiscard]] T& operator*() {
222  auto* const ptr = get_ptr_helper(*this);
223  if (ptr) {
224  return *ptr;
225  } else {
226  std::ostringstream oss;
227  oss << *this << " attempt to dereference [T& operator*]";
228  throw std::runtime_error(oss.str());
229  }
230  }
231 
232  [[nodiscard]] T const& operator*() const {
233  auto* const ptr = get_ptr_helper(*this);
234  if (ptr) {
235  return *ptr;
236  } else {
237  std::ostringstream oss;
238  oss << *this << " attempt to dereference [T const& operator*]";
239  throw std::runtime_error(oss.str());
240  }
241  }
242 
243  [[nodiscard]] explicit operator T*() {
244  return get_ptr_helper(*this);
245  }
246 
247  [[nodiscard]] explicit operator T const *() const {
248  return get_ptr_helper(*this);
249  }
250 
251  friend std::ostream& operator<<(std::ostream& os, data_handle const& dh) {
252  os << "data_handle<" << cxx_demangle(typeid(T).name()) << ">{";
253  if (auto const valid = dh.m_offset; valid || dh.m_offset.was_once_valid()) {
254  auto* const container_data = dh.container_data();
255  if (auto const maybe_info = utils::find_container_info(container_data)) {
256  if (!maybe_info->container().empty()) {
257  os << "cont=" << maybe_info->container() << ' ';
258  }
259  // the printout will show the logical row number, but we have the physical size.
260  // these are different in case of array variables. convert the size to a logical
261  // one, but add some printout showing what we did
262  auto size = maybe_info->size();
263  assert(dh.m_array_dim >= 1);
265  assert(size % dh.m_array_dim == 0);
266  size /= dh.m_array_dim;
267  os << maybe_info->field();
268  if (dh.m_array_dim > 1) {
269  os << '[' << dh.m_array_index << '/' << dh.m_array_dim << ']';
270  }
271  os << ' ' << dh.m_offset << '/' << size;
272  } else {
273  os << "cont=" << (container_data ? "unknown " : "deleted ") << dh.m_offset
274  << "/unknown";
275  }
276  // print the value if it exists and has an output operator
277  if (valid) {
278  // if the referred-to *column* was deleted but the referred-to *row* is still valid,
279  // valid == true but ptr == nullptr.
280  if (auto* const ptr = get_ptr_helper(dh); ptr) {
281  detail::print_value(os, *ptr);
282  }
283  }
284  } else if (dh.m_container_or_raw_ptr) {
285  os << "raw=" << dh.m_container_or_raw_ptr;
286  } else {
287  os << dh.m_offset;
288  }
289  return os << '}';
290  }
291 
292  // TODO should a "modern" handle that has become invalid compare equal to a
293  // null handle that was never valid? Perhaps yes, as both evaluate to
294  // boolean false, but their string representations are different.
295  [[nodiscard]] friend bool operator==(data_handle const& lhs, data_handle const& rhs) {
296  return lhs.m_offset == rhs.m_offset &&
297  lhs.m_container_or_raw_ptr == rhs.m_container_or_raw_ptr &&
298  lhs.m_array_dim == rhs.m_array_dim && lhs.m_array_index == rhs.m_array_index;
299  }
300 
301  [[nodiscard]] friend bool operator!=(data_handle const& lhs, data_handle const& rhs) {
302  return !(lhs == rhs);
303  }
304 
305  /**
306  * @brief Get the identifier used by this handle.
307  *
308  * This is likely to only be useful for the (hopefully temporary) method
309  * neuron::container::notify_when_handle_dies.
310  */
312  return m_offset;
313  }
314 
315  private:
316  friend struct generic_data_handle;
317  friend struct std::hash<data_handle>;
319  // If m_offset is/was valid for a modern container, this is a pointer to a value containing the
320  // start of the underlying contiguous storage (i.e. the return value of std::vector<T>::data())
321  // otherwise it is possibly-null T*
323  // These are needed for "modern" handles to array variables, where the offset
324  // yielded by m_offset needs to be scaled/shifted by an array dimension/index
325  // before being applied to m_container_or_raw_ptr
327 };
328 
329 /**
330  * @brief Explicit specialisation data_handle<void>.
331  *
332  * This is convenient as it allows void* to be stored in generic_data_handle.
333  * The "modern style" data handles that hold a reference to a container and a way of determining an
334  * offset into that container do not make sense with a void value type, so this only supports the
335  * "legacy" mode where a data handle wraps a plain pointer.
336  */
337 template <>
338 struct data_handle<void> {
339  data_handle() = default;
341  : m_raw_ptr{raw_ptr} {}
343  : m_raw_ptr{raw_ptr} {}
344  [[nodiscard]] bool refers_to_a_modern_data_structure() const {
345  return false;
346  }
347  explicit operator bool() const {
348  return m_raw_ptr;
349  }
350  explicit operator void*() {
351  return m_raw_ptr;
352  }
353  explicit operator void const *() const {
354  return m_raw_ptr;
355  }
356  friend std::ostream& operator<<(std::ostream& os, data_handle<void> const& dh) {
357  return os << "data_handle<void>{raw=" << dh.m_raw_ptr << '}';
358  }
359  friend bool operator==(data_handle<void> const& lhs, data_handle<void> const& rhs) {
360  return lhs.m_raw_ptr == rhs.m_raw_ptr;
361  }
362 
363  private:
364  friend struct generic_data_handle;
365  friend struct std::hash<data_handle<void>>;
366  void* m_raw_ptr;
367 };
368 
369 } // namespace neuron::container
370 
371 // Enable data_handle<T> as a key type in std::unordered_map
372 template <typename T>
373 struct std::hash<neuron::container::data_handle<T>> {
374  std::size_t operator()(neuron::container::data_handle<T> const& s) const noexcept {
375  static_assert(sizeof(std::size_t) == sizeof(T const*));
376  if (s.m_offset || s.m_offset.was_once_valid()) {
377  // The hash should not include the current row number, but rather the
378  // std::size_t* that is dereferenced to *get* the current row number,
379  // and which container this generic value lives in.
381  s.m_offset) ^
382  reinterpret_cast<std::size_t>(s.m_container_or_raw_ptr);
383  } else {
384  return reinterpret_cast<std::size_t>(s.m_container_or_raw_ptr);
385  }
386  }
387 };
388 
389 template <>
390 struct std::hash<neuron::container::data_handle<void>> {
391  std::size_t operator()(neuron::container::data_handle<void> const& s) const noexcept {
392  static_assert(sizeof(std::size_t) == sizeof(void*));
393  return reinterpret_cast<std::size_t>(s.m_raw_ptr);
394  }
395 };
int cxx_demangle(const char *symbol, char **funcname, size_t *funcname_sz)
#define assert(ex)
Definition: hocassrt.h:24
#define rhs
Definition: lineq.h:6
static double valid(void *v)
Definition: linmod1.cpp:46
const char * name
Definition: init.cpp:16
void move(Item *q1, Item *q2, Item *q3)
Definition: list.cpp:200
std::ostream & print_value(std::ostream &os, T const &val)
Definition: data_handle.hpp:26
std::ostream & print_value_impl(std::ostream &os, T const &,...)
Definition: data_handle.hpp:18
data_handle< T > find_data_handle(T *ptr)
std::unique_ptr< storage_info > find_container_info(void const *)
Try and find a helpful name for a container.
Definition: container.cpp:149
constexpr do_not_search_t do_not_search
Definition: data_handle.hpp:11
In mechanism libraries, cannot use auto const token = nrn_ensure_model_data_are_sorted(); because the...
Definition: tnode.hpp:17
s
Definition: multisend.cpp:521
Explicit specialisation data_handle<void>.
data_handle(do_not_search_t, void *raw_ptr)
friend std::ostream & operator<<(std::ostream &os, data_handle< void > const &dh)
friend bool operator==(data_handle< void > const &lhs, data_handle< void > const &rhs)
Stable handle to a generic value.
Definition: data_handle.hpp:61
non_owning_identifier_without_container identifier() const
Get the identifier used by this handle.
friend bool operator!=(data_handle const &lhs, data_handle const &rhs)
static auto get_ptr_helper(This &this_ref)
bool refers_to_a_modern_data_structure() const
Query whether this data handle is in "modern" mode.
data_handle(do_not_search_t, T *raw_ptr)
Create a data_handle<T> wrapping the raw T*.
Definition: data_handle.hpp:91
T const * container_data() const
data_handle(non_owning_identifier_without_container offset, T *const *container, int array_dim, int array_index)
friend bool operator==(data_handle const &lhs, data_handle const &rhs)
bool refers_to(Container const &container) const
Query whether this generic handle points to a value from the Tag field of the given container.
data_handle(T *raw_ptr)
Construct a data_handle from a plain pointer.
Definition: data_handle.hpp:66
friend std::ostream & operator<<(std::ostream &os, data_handle const &dh)
std::size_t current_row() const
Get the current logical row number.
non_owning_identifier_without_container m_offset
data_handle next_array_element(int shift=1) const
Get a data handle to a different element of the same array variable.
Non-template stable handle to a generic value.
A non-owning permutation-stable identifier for an entry in a container.
bool was_once_valid() const
Did the identifier use to refer to a valid entry?
std::size_t operator()(neuron::container::data_handle< T > const &s) const noexcept
std::size_t operator()(neuron::container::data_handle< void > const &s) const noexcept