NEURON
generic_data_handle.hpp
Go to the documentation of this file.
1 #pragma once
2 #include "backtrace_utils.h"
5 
6 #include <cstddef>
7 #include <cstring>
8 #include <typeinfo>
9 #include <type_traits>
10 
11 namespace neuron::container {
12 /**
13  * @brief Non-template stable handle to a generic value.
14  *
15  * This is a type-erased version of data_handle<T>, with the additional feature
16  * that it can store values of POD types no larger than a pointer (typically int
17  * and float). It stores (at runtime) the type of the value it contains, and is
18  * therefore type-safe, but this increases sizeof(generic_data_handle) by 50% so
19  * it may be prudent to view this as useful for validation/debugging but not
20  * something to become too dependent on.
21  *
22  * There are several states that instances of this class can be in:
23  * - null, no value is contained, any type can be assigned without any type
24  * mismatch error (m_type=null, m_container=null, m_offset=null)
25  * - wrapping an instance of a small, trivial type T (m_type=&typeid(T),
26  * m_container=the_value, m_offset=null)
27  * - wrapping a data handle<T> (m_type=&typeid(T*), m_container=the_container,
28  * m_offset=ptr_to_row)
29  *
30  * @todo Consider whether this should be made more like std::any (with a maximum
31  * 2*sizeof(void*) and a promise never to allocate memory dynamically) so
32  * it actually has a data_handle<T> subobject. Presumably that would mean
33  * data_handle<T> would need to have a trivial destructor. This might make
34  * it harder in future to have some vector_of_generic_data_handle type
35  * that hoists out the pointer-to-container and typeid parts that should
36  * be the same for all rows.
37  */
39  private:
40  // The exact criteria could be refined, it is definitely not possible to
41  // store types with non-trivial destructors.
42  template <typename T>
43  static constexpr bool can_be_stored_literally_v =
44  std::is_trivial_v<T> && !std::is_pointer_v<T> && sizeof(T) <= sizeof(void*);
45 
46  public:
47  /** @brief Construct a null data handle.
48  */
49  generic_data_handle() = default;
50 
51  /** @brief Construct a null data handle.
52  */
53  generic_data_handle(std::nullptr_t) {}
54 
55  /**
56  * @brief Construct a generic data handle that holds a small literal value.
57  *
58  * This is explicit to avoid things like operator<<(ostream&, generic_data_handle const&) being
59  * considered when printing values like size_t.
60  */
61  template <typename T, std::enable_if_t<can_be_stored_literally_v<T>, int> = 0>
63  : m_type{&typeid(T)} {
64  std::memcpy(&m_container, &value, sizeof(T));
65  }
66 
67  /**
68  * @brief Assign a small literal value to a generic data handle.
69  *
70  * This is important when generic_data_handle is used as the Datum type for "pointer" variables
71  * in MOD files.
72  */
73  template <typename T, std::enable_if_t<can_be_stored_literally_v<T>, int> = 0>
75  return *this = generic_data_handle{value};
76  }
77 
78  /**
79  * @brief Store a pointer inside this generic data handle.
80  *
81  * Explicit handling of pointer types (with redirection via data_handle<T>) ensures that
82  * `some_generic_handle = some_ptr_to_T` promotes the raw `T*` to a modern `data_handle<T>` that
83  * is stable to permutation.
84  */
85  template <typename T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
88  }
89 
90  template <typename T>
92  : generic_data_handle{data_handle<T>{dns, raw_ptr}} {}
93 
94  /**
95  * @brief Wrap a data_handle<void> in a generic data handle.
96  *
97  * Note that data_handle<void> is always wrapping a raw void* and is never
98  * in "modern" mode
99  */
100  template <typename T, std::enable_if_t<std::is_same_v<T, void>, int> = 0>
102  : m_container{handle.m_raw_ptr}
103  , m_type{&typeid(T*)} {
104  static_assert(std::is_same_v<T, void>);
105  }
106 
107  /**
108  * @brief Wrap a data_handle<T != void> in a generic data handle.
109  */
110  template <typename T, std::enable_if_t<!std::is_same_v<T, void>, int> = 0>
112  : m_offset{handle.m_offset}
113  , m_container{handle.m_container_or_raw_ptr}
114  , m_type{&typeid(T*)}
115  , m_array_dim{handle.m_array_dim}
116  , m_array_index{handle.m_array_index} {
117  static_assert(!std::is_same_v<T, void>);
118  }
119 
120  /**
121  * @brief Create data_handle<T> from a generic data handle.
122  *
123  * The conversion will succeed, yielding a null data_handle<T>, if the
124  * generic data handle is null. If the generic data handle is not null then
125  * the conversion will succeed if the generic data handle actually holds a
126  * data_handle<T> or a literal T*.
127  *
128  * It might be interesting in future to explore dropping m_type in optimised
129  * builds, in which case we should aim to avoid predicating important logic
130  * on exceptions thrown by this function.
131  */
132  template <typename T>
133  explicit operator data_handle<T>() const {
134  // Either the type has to match or the generic_data_handle needs to have
135  // never been given a type
136  if (!m_type) {
137  // A (typeless / default-constructed) null generic_data_handle can
138  // be converted to any (null) data_handle<T>.
139  return {};
140  }
141  if (typeid(T*) != *m_type) {
142  throw_error(" cannot be converted to data_handle<" + cxx_demangle(typeid(T).name()) +
143  ">");
144  }
145  if (m_offset.has_always_been_null()) {
146  // This is a data handle in backwards-compatibility mode, wrapping a
147  // raw pointer of type T*, or a null handle that has always been null (as opposed to a
148  // handle that became null). Passing do_not_search prevents the data_handle<T>
149  // constructor from trying to find the raw pointer in the NEURON data structures.
150  return {do_not_search, static_cast<T*>(m_container)};
151  }
152  // Nothing after here is reachable with T=void, as data_handle<void> is always either null
153  // or in backwards-compatibility mode. the branch structure has been chosen to try and
154  // minimise compiler warnings and maximise reported coverage...
155  if constexpr (!std::is_same_v<T, void>) {
156  if (!m_offset.was_once_valid()) {
157  // A real and still-valid data handle. This cannot be instantiated with T=void
158  // because data_handle<void> does not have a 4-argument constructor.
159  assert(m_container);
160  return {m_offset, static_cast<T* const*>(m_container), m_array_dim, m_array_index};
161  }
162  }
163  // Reaching here should mean T != void && was_once_valid == true, i.e. this used to be a
164  // valid data handle, but it has since been invalidated. Invalid data handles never become
165  // valid again, so we can safely produce a "fully null" data_handle<T>.
166  return {};
167  }
168 
169  /** @brief Explicit conversion to any T.
170  *
171  * It might be interesting in future to explore dropping m_type in
172  * optimised builds, in which case we should aim to avoid predicating
173  * important logic on exceptions thrown by this function.
174  *
175  * Something like static_cast<double*>(generic_handle) will work both if
176  * the Datum holds a literal double* and if it holds a data_handle<double>.
177  *
178  * @todo Consider conversion to bool and whether this means not-null or to
179  * obtain a literal, wrapped bool value
180  */
181  template <typename T>
182  T get() const {
183  if constexpr (std::is_pointer_v<T>) {
184  // If T=U* (i.e. T is a pointer type) then we might be in modern
185  // mode, go via data_handle<U>
186  return static_cast<T>(static_cast<data_handle<std::remove_pointer_t<T>>>(*this));
187  } else {
188  // Getting a literal value saved in m_container
189  static_assert(can_be_stored_literally_v<T>,
190  "generic_data_handle can only hold non-pointer types that are trivial "
191  "and smaller than a pointer");
192  if (!m_offset.has_always_been_null()) {
193  throw_error(" conversion to " + cxx_demangle(typeid(T).name()) +
194  " not possible for a handle [that was] in modern mode");
195  }
196  if (typeid(T) != *m_type) {
197  throw_error(" does not hold a literal value of type " +
198  cxx_demangle(typeid(T).name()));
199  }
200  T ret{};
201  std::memcpy(&ret, &m_container, sizeof(T));
202  return ret;
203  }
204  }
205 
206  // Defined elsewhere to optimise compile times.
207  friend std::ostream& operator<<(std::ostream& os, generic_data_handle const& dh);
208 
209  /** @brief Check if this handle refers to the specific type.
210  *
211  * This could be a small, trivial type (e.g. T=int) or a pointer type (e.g.
212  * T=double*). holds<double*>() == true could either indicate that a
213  * data_handle<double> is held or that a literal double* is held.
214  *
215  * It might be interesting in future to explore dropping m_type in
216  * optimised builds, in which case we should aim to avoid predicating
217  * important logic on this function.
218  */
219  template <typename T>
220  [[nodiscard]] bool holds() const {
221  return m_type && typeid(T) == *m_type;
222  }
223 
224  /** @brief Check if this handle contains a data_handle<T> or just a literal.
225  *
226  * This should match
227  * static_cast<data_handle<T>>(generic_handle).refers_to_a_modern_data_structure()
228  * if T is correct. This will return true if we hold a data_handle that
229  * refers to a since-deleted row.
230  */
231  [[nodiscard]] bool refers_to_a_modern_data_structure() const {
232  return !m_offset.has_always_been_null();
233  }
234 
235  /** @brief Return the demangled name of the type this handle refers to.
236  *
237  * If the handle contains data_handle<T>, this will be T*. If a literal
238  * value or raw pointer is being wrapped, that possibly-pointer type will
239  * be returned.
240  *
241  * It might be interesting in future to explore dropping m_type in
242  * optimised builds, in which case we should aim to avoid predicating
243  * important logic on this function.
244  */
245  [[nodiscard]] std::string type_name() const {
246  return m_type ? cxx_demangle(m_type->name()) : "typeless_null";
247  }
248 
249  /** @brief Obtain a reference to the literal value held by this handle.
250  *
251  * Storing literal values is incompatible with storing data_handle<T>. If
252  * the handle stores data_handle<T> then calling this method throws an
253  * exception. If the handle is null, this sets the stored type to be T and
254  * returns a reference to it. If the handle already holds a literal value
255  * of type T then a reference to it is returned.
256  *
257  * Note that, unlike converting to double*, literal_value<double*>() will
258  * fail if the handle contains data_handle<double>, as in that case there
259  * is no persistent double* that could be referred to.
260  *
261  * It might be interesting in future to explore dropping m_type in
262  * optimised builds, in which case we should aim to avoid predicating
263  * important logic on exceptions thrown by this function.
264  */
265  template <typename T>
266  [[nodiscard]] T& literal_value() {
267  if (!m_offset.has_always_been_null()) {
268  throw_error("::literal_value<" + cxx_demangle(typeid(T).name()) +
269  "> cannot be called on a handle [that was] in modern mode");
270  } else {
271  // This is a data handle in backwards-compatibility mode, wrapping a
272  // raw pointer, or a null data handle. Using raw_ptr() on a typeless_null
273  // (default-constructed) handle turns it into a legacy handle-to-T.
274  if (!m_type) {
275  m_type = &typeid(T);
276  } else if (typeid(T) != *m_type) {
277  throw_error(" does not hold a literal value of type " +
278  cxx_demangle(typeid(T).name()));
279  }
280  return *reinterpret_cast<T*>(&m_container);
281  }
282  }
283 
284  /**
285  * @brief Is the generic data handle a data_handle and invalid?
286  *
287  * If it contains a value (convertible to) a `data_handle<double>`, then
288  * it's valid if and only if it's a valid `data_handle<double>`.
289  *
290  * If it doesn't contain a data handle, return false.
291  */
292  bool is_invalid_handle() const {
293  if (!m_type) {
294  // Empty default initialized.
295  return true;
296  }
297 
298  return holds<double*>() ? !bool(data_handle<double>(*this)) : false;
299  }
300 
301  private:
302  [[noreturn]] void throw_error(std::string message) const {
303  std::ostringstream oss{};
304  oss << *this << std::move(message);
305  throw std::runtime_error(std::move(oss).str());
306  }
307  // Offset into the underlying storage container. If this handle is holding a
308  // literal value, such as a raw pointer, then this will be null.
310  // T* const* for the T encoded in m_type if m_offset is non-null,
311  // otherwise a literal value is stored in this space.
312  void* m_container{};
313  // Pointer to typeid(T) for the wrapped type
314  std::type_info const* m_type{};
315  // Extra information required for data_handle<T> to point at array variables
316  int m_array_dim{1}, m_array_index{};
317 };
318 
319 namespace utils {
320 namespace detail {
321 /**
322  * @brief Try and promote a generic_data_handle wrapping a raw pointer.
323  *
324  * If the raw pointer can be found in the model data structures, a "modern" permutation-stable
325  * handle to it will be returned. If it cannot be found, a null generic_data_handle will be
326  * returned.
327  */
329 } // namespace detail
330 // forward declared in model_data_fwd.hpp
331 template <typename T>
332 [[nodiscard]] data_handle<T> find_data_handle(T* ptr) {
333  return static_cast<data_handle<T>>(detail::promote_or_clear({do_not_search, ptr}));
334 }
335 } // namespace utils
336 } // namespace neuron::container
int cxx_demangle(const char *symbol, char **funcname, size_t *funcname_sz)
#define assert(ex)
Definition: hocassrt.h:24
const char * name
Definition: init.cpp:16
void move(Item *q1, Item *q2, Item *q3)
Definition: list.cpp:200
handle_interface< non_owning_identifier< storage > > handle
Non-owning handle to a Mechanism instance.
generic_data_handle promote_or_clear(generic_data_handle)
Try and promote a generic_data_handle wrapping a raw pointer.
Definition: container.cpp:126
data_handle< T > find_data_handle(T *ptr)
std::ostream & operator<<(std::ostream &os, generic_data_handle const &dh)
Definition: container.cpp:62
constexpr do_not_search_t do_not_search
Definition: data_handle.hpp:11
static uint32_t value
Definition: scoprand.cpp:25
Stable handle to a generic value.
Definition: data_handle.hpp:61
Non-template stable handle to a generic value.
bool holds() const
Check if this handle refers to the specific type.
T & literal_value()
Obtain a reference to the literal value held by this handle.
bool refers_to_a_modern_data_structure() const
Check if this handle contains a data_handle<T> or just a literal.
void throw_error(std::string message) const
bool is_invalid_handle() const
Is the generic data handle a data_handle and invalid?
generic_data_handle(std::nullptr_t)
Construct a null data handle.
T get() const
Explicit conversion to any T.
generic_data_handle(do_not_search_t dns, T *raw_ptr)
generic_data_handle & operator=(T value)
Assign a small literal value to a generic data handle.
generic_data_handle(T value)
Construct a generic data handle that holds a small literal value.
generic_data_handle(data_handle< T > const &handle)
Wrap a data_handle<void> in a generic data handle.
std::string type_name() const
Return the demangled name of the type this handle refers to.
generic_data_handle()=default
Construct a null data handle.
A non-owning permutation-stable identifier for an entry in a container.