NEURON
node.cpp
Go to the documentation of this file.
3 #include "section.h"
4 
5 #include <catch2/generators/catch_generators.hpp>
6 #include <catch2/catch_test_macros.hpp>
7 
8 #include <iostream>
9 #include <numeric>
10 #include <optional>
11 #include <random>
12 #include <sstream>
13 #include <vector>
14 #include <unordered_map>
15 
16 static_assert(std::is_default_constructible_v<Node>);
17 static_assert(!std::is_copy_constructible_v<Node>);
18 static_assert(std::is_move_constructible_v<Node>);
19 static_assert(!std::is_copy_assignable_v<Node>);
20 static_assert(std::is_move_assignable_v<Node>);
21 static_assert(std::is_destructible_v<Node>);
22 
23 using namespace neuron::container;
24 using namespace neuron::container::Node;
25 
26 // We want to check that the tests pass for all of:
27 // - data_handle<T>
28 // - data_handle<T> -> T* -> data_handle<T>
29 // - data_handle<T> -> generic_data_handle -> data_handle<T>
31 template <typename T>
33  if (type == Transform::None) {
34  return handle;
35  } else if (type == Transform::ViaRawPointer) {
36  return data_handle<T>{static_cast<T*>(handle)};
37  } else {
39  return static_cast<data_handle<T>>(generic_data_handle{handle});
40  }
41 }
42 
43 constexpr static double magic_voltage_value = 42.;
44 
45 template <typename T>
46 static std::string to_str(T const& x) {
47  std::ostringstream oss;
48  oss << x;
49  return oss.str();
50 }
51 
52 TEST_CASE("data_handle<double>", "[Neuron][data_structures][data_handle]") {
53  GIVEN("A null handle") {
56  GENERATE(Transform::None,
59  THEN("Check it is really null") {
60  REQUIRE_FALSE(handle);
61  }
62  THEN("Check it compares equal to a different null pointer") {
63  data_handle<double> const other_handle{};
64  REQUIRE(handle == other_handle);
65  }
66  THEN("Check it prints the right value") {
67  REQUIRE(to_str(handle) == "data_handle<double>{null}");
68  }
69  THEN("Check it doesn't claim to be modern") {
70  REQUIRE_FALSE(handle.refers_to_a_modern_data_structure());
71  }
72  THEN("Check it decays to a null pointer") {
73  auto* foo_ptr = static_cast<double*>(handle);
74  REQUIRE(foo_ptr == nullptr);
75  }
76  }
77  GIVEN("A handle wrapping a raw pointer (compatibility mode)") {
78  std::vector<double> foo(10);
79  std::iota(foo.begin(), foo.end(), magic_voltage_value);
80 
81  data_handle<double> handle{foo.data()};
83  GENERATE(Transform::None,
86  THEN("Check it is not null") {
87  REQUIRE(handle);
88  }
89  THEN("Check it does not compare equal to a null handle") {
90  data_handle<double> null_handle{};
91  REQUIRE(handle != null_handle);
92  }
93  THEN("Check it compares equal to a different handle wrapping the same raw pointer") {
94  data_handle<double> other_handle{foo.data()};
95  REQUIRE(handle == other_handle);
96  }
97  THEN("Check it yields the right value") {
98  REQUIRE(*handle == magic_voltage_value);
99  }
100  THEN("Check it doesn't claim to be modern") {
101  REQUIRE_FALSE(handle.refers_to_a_modern_data_structure());
102  REQUIRE_FALSE(handle.refers_to<neuron::container::Node::field::Voltage>(
103  neuron::model().node_data()));
104  }
105  THEN("Check it decays to the right raw pointer") {
106  auto* foo_ptr = static_cast<double*>(handle);
107  REQUIRE(foo_ptr == foo.data());
108  }
109  THEN("Check it prints the right value") {
110  std::ostringstream expected;
111  expected << "data_handle<double>{raw=" << foo.data() << '}';
112  REQUIRE(to_str(handle) == expected.str());
113  }
114  THEN("Check that we can store/retrieve in/from unordered_map") {
115  std::unordered_map<data_handle<double>, std::string> map;
116  map[handle] = "unordered_map";
117  REQUIRE(map[handle] == "unordered_map");
118  }
119  THEN("Check that next_array_element works") {
120  auto next = handle.next_array_element(5);
121  REQUIRE(next);
122  REQUIRE(*next == magic_voltage_value + 5);
123  }
124  }
125  GIVEN("A handle to a void pointer") {
126  auto foo = std::make_shared<double>(magic_voltage_value);
127 
130  GENERATE(Transform::None,
133  THEN("Check it is not null") {
134  REQUIRE(handle);
135  }
136  THEN("Check it doesn't claim to be modern") {
137  REQUIRE_FALSE(handle.refers_to_a_modern_data_structure());
138  }
139  THEN("Check it decays to the right raw pointer") {
140  auto* foo_ptr = static_cast<void*>(handle);
141  REQUIRE(foo_ptr == foo.get());
142  }
143  THEN("Check it matches another data_handle<void> to same pointer") {
144  const data_handle<void> other_handle{foo.get()};
145  REQUIRE(handle == other_handle);
146  }
147  THEN("Check it prints the right value") {
148  std::ostringstream expected;
149  expected << "data_handle<void>{raw=" << foo.get() << '}';
150  REQUIRE(to_str(handle) == expected.str());
151  }
152  }
153  GIVEN("A handle referring to an entry in an SOA container") {
154  REQUIRE(neuron::model().node_data().size() == 0);
155  std::optional<::Node> node{std::in_place};
157  auto handle = node->v_handle();
158  const auto handle_id = handle.identifier();
160  GENERATE(Transform::None,
163  THEN("Check it is not null") {
164  REQUIRE(handle);
165  }
166  THEN("Check it actually refers_to voltage and not something else") {
168  neuron::model().node_data()));
169  REQUIRE_FALSE(handle.refers_to<neuron::container::Node::field::Area>(
170  neuron::model().node_data()));
171  }
172  THEN("Check it does not compare equal to a null handle") {
173  data_handle<double> null_handle{};
174  REQUIRE(handle != null_handle);
175  }
176  THEN("Check it does not compare equal to a handle in legacy mode") {
177  double foo{};
178  data_handle<double> foo_handle{&foo};
179  REQUIRE(handle != foo_handle);
180  }
181  THEN("Check it yields the right value") {
182  REQUIRE(*handle == magic_voltage_value);
183  const auto const_handle(handle);
184  REQUIRE(*const_handle == magic_voltage_value);
185  }
186  THEN("Check it claims to be modern") {
187  REQUIRE(handle.refers_to_a_modern_data_structure());
188  }
189  THEN("Check it prints the right value") {
190  REQUIRE(to_str(handle) == "data_handle<double>{Node::field::Voltage row=0/1 val=42}");
191  }
192  THEN("Check that getting next_array_element throws, dimension is 1") {
193  REQUIRE_THROWS(handle.next_array_element());
194  }
195  THEN("Check that we can store/retrieve in/from unordered_map") {
196  std::unordered_map<data_handle<double>, std::string> map;
197  map[handle] = "unordered_map_modern_dh";
198  REQUIRE(map[handle] == "unordered_map_modern_dh");
199  }
200  THEN("Make sure we get the current logical row number") {
201  REQUIRE(handle.current_row() == 0);
202  }
203  THEN("Check that deleting the (Node) object it refers to invalidates the handle") {
204  node.reset(); // delete the underlying Node object
205  REQUIRE_FALSE(handle);
206  // REQUIRE(handle == data_handle<double>{});
207  REQUIRE(handle.refers_to_a_modern_data_structure());
208  REQUIRE(to_str(handle) == "data_handle<double>{Node::field::Voltage died/0}");
210  neuron::model().node_data()));
211  REQUIRE_THROWS(*handle);
212  const auto const_handle(handle);
213  REQUIRE_THROWS(*const_handle);
214  REQUIRE(handle.identifier() == handle_id);
215  }
216  THEN(
217  "Check that mutating the underlying container while holding a raw pointer has the "
218  "expected effect") {
219  auto* raw_ptr = static_cast<double*>(handle);
220  REQUIRE(raw_ptr);
221  REQUIRE(*raw_ptr == magic_voltage_value);
222  node.reset(); // delete the underlying Node object, handle is now invalid
223  REQUIRE_FALSE(handle);
224  REQUIRE(raw_ptr); // no magic here, we have a dangling pointer
225  data_handle<double> new_handle{raw_ptr};
226  REQUIRE(new_handle); // handle refers to no-longer-valid memory, but we can't detect
227  // that
228  REQUIRE(handle != new_handle);
229  REQUIRE_FALSE(new_handle.refers_to_a_modern_data_structure());
230  // dereferencing raw_ptr is undefined behaviour
231  }
232  }
233 }
234 
235 namespace neuron::test {
236 std::vector<double> get_node_voltages(std::vector<::Node> const& nodes) {
237  std::vector<double> ret{};
238  std::transform(nodes.begin(), nodes.end(), std::back_inserter(ret), [](auto const& node) {
239  return node.v();
240  });
241  return ret;
242 }
243 std::tuple<std::vector<::Node>, std::vector<double>> get_nodes_and_reference_voltages(
244  std::size_t num_nodes = 10) {
245  std::vector<double> reference_voltages{};
246  std::generate_n(std::back_inserter(reference_voltages), num_nodes, [i = 0]() mutable {
247  auto x = i++;
248  return x * x;
249  });
250  std::vector<::Node> nodes{};
251  std::transform(reference_voltages.begin(),
252  reference_voltages.end(),
253  std::back_inserter(nodes),
254  [](auto v) {
255  ::Node node{};
256  node.v() = v;
257  return node;
258  });
259  return {std::move(nodes), std::move(reference_voltages)};
260 }
261 } // namespace neuron::test
262 
263 TEST_CASE("SOA-backed Node structure", "[Neuron][data_structures][node]") {
264  REQUIRE(neuron::model().node_data().size() == 0);
265  GIVEN("A default-constructed node") {
266  ::Node node{};
267  THEN("Check its SOA-backed members have their default values") {
268  REQUIRE(node.area() == field::Area{}.default_value());
269  REQUIRE(node.v() == field::Voltage{}.default_value());
270  }
271  THEN("Check we can get a non-owning handle to it") {
272  auto handle = node.non_owning_handle();
273  AND_THEN("Check the handle yields the corect values") {
274  REQUIRE(handle.area() == field::Area{}.default_value());
275  REQUIRE(handle.v() == field::Voltage{}.default_value());
276  }
277  }
278  }
279  GIVEN("A series of nodes with increasing integer voltages") {
281  auto nodes_and_voltages = neuron::test::get_nodes_and_reference_voltages();
282  auto& nodes = std::get<0>(nodes_and_voltages);
283  auto& reference_voltages = std::get<1>(nodes_and_voltages);
284  auto& node_data = neuron::model().node_data();
285  // Flag this original order as "sorted" so that the tests that it is no
286  // longer sorted after permutation are meaningful.
287  {
288  auto write_token = node_data.issue_frozen_token();
289  node_data.mark_as_sorted(write_token);
290  }
291  auto const require_logical_match = [&]() {
292  THEN("Check the logical voltages still match") {
293  REQUIRE(get_node_voltages(nodes) == reference_voltages);
294  }
295  };
296  auto const storage_match = [&]() {
297  for (auto i = 0; i < nodes.size(); ++i) {
298  if (node_data.get<field::Voltage>(i) != reference_voltages.at(i)) {
299  return false;
300  }
301  }
302  return true;
303  };
304  auto const require_logical_and_storage_match = [&]() {
305  THEN("Check the logical voltages still match") {
306  REQUIRE(get_node_voltages(nodes) == reference_voltages);
307  AND_THEN("Check the underlying storage also matches") {
308  REQUIRE(storage_match());
309  }
310  }
311  };
312  auto const require_logical_match_and_storage_different = [&]() {
313  THEN("Check the logical voltages still match") {
314  REQUIRE(get_node_voltages(nodes) == reference_voltages);
315  AND_THEN("Check the underlying storage no longer matches") {
316  REQUIRE_FALSE(storage_match());
317  }
318  }
319  };
320  WHEN("Values are read back immediately") {
321  require_logical_and_storage_match();
322  }
323  std::vector<std::size_t> perm_vector(nodes.size());
324  std::iota(perm_vector.begin(), perm_vector.end(), 0);
325  WHEN("The underlying storage is rotated") {
326  auto rotated = perm_vector;
327  std::rotate(rotated.begin(), std::next(rotated.begin()), rotated.end());
328  auto const sorted_token = node_data.apply_reverse_permutation(std::move(rotated));
329  require_logical_match_and_storage_different();
330  }
331  WHEN("A unit reverse permutation is applied to the underlying storage") {
332  node_data.apply_reverse_permutation(std::move(perm_vector));
333  require_logical_and_storage_match();
334  // Should the data still be sorted here or not? Should
335  // apply_permutation bother checking if the permutation did
336  // anything?
337  }
338  WHEN("A random permutation is applied to the underlying storage") {
339  std::mt19937 g{42};
340  std::shuffle(perm_vector.begin(), perm_vector.end(), g);
341  auto const sorted_token = node_data.apply_reverse_permutation(std::move(perm_vector));
342  // the permutation is random, so we don't know if voltage_storage
343  // will match reference_voltages or not
344  require_logical_match();
345  }
346  auto const require_exception = [&](auto perm) {
347  THEN("An exception is thrown") {
348  REQUIRE_THROWS(node_data.apply_reverse_permutation(std::move(perm)));
349  AND_THEN("The container is still flagged as sorted") {
350  REQUIRE(node_data.is_sorted());
351  }
352  }
353  };
354  WHEN("A too-short permutation is applied to the underlying storage") {
355  std::vector<std::size_t> bad_perm(nodes.size() - 1);
356  std::iota(bad_perm.begin(), bad_perm.end(), 0);
357  require_exception(std::move(bad_perm));
358  }
359  WHEN("A permutation with a repeated entry is applied to the underlying storage") {
360  std::vector<std::size_t> bad_perm(nodes.size());
361  std::iota(bad_perm.begin(), bad_perm.end(), 0);
362  bad_perm[0] = 1; // repeated entry
363  require_exception(std::move(bad_perm));
364  }
365  WHEN("A permutation with an invalid value is applied to the underlying storage") {
366  std::vector<std::size_t> bad_perm(nodes.size());
367  std::iota(bad_perm.begin(), bad_perm.end(), 0);
368  bad_perm[0] = std::numeric_limits<std::size_t>::max(); // out of range
369  require_exception(std::move(bad_perm));
370  }
371  WHEN("The last Node is removed") {
372  nodes.pop_back();
373  reference_voltages.pop_back();
374  require_logical_and_storage_match();
375  }
376  WHEN("The first Node is removed") {
377  nodes.erase(nodes.begin());
378  reference_voltages.erase(reference_voltages.begin());
379  require_logical_match_and_storage_different();
380  }
381  WHEN("The middle Node is removed") {
382  auto const index_to_remove = nodes.size() / 2;
383  nodes.erase(std::next(nodes.begin(), index_to_remove));
384  reference_voltages.erase(std::next(reference_voltages.begin(), index_to_remove));
385  require_logical_match_and_storage_different();
386  }
387  WHEN("The dense storage is sorted and marked read-only") {
388  // A rough sketch of the concept here is that if we have a
389  // SOA-backed quantity, like the node voltages, then referring
390  // stably to those values requires something like
391  // data_handle<double>. We might hold some complicated structure of
392  // those "in the interpreter", let's say
393  // std::list<data_handle<double>>, and want to flatten that into
394  // something simpler for use in the translated MOD file code --
395  // let's say std::vector<double*> -- while the data remain "sorted".
396  {
397  // Label the current order as sorted and acquire a token that
398  // freezes it that way. The data should be sorted until the
399  // token goes out of scope.
400  auto frozen_token = node_data.issue_frozen_token();
401  node_data.mark_as_sorted(frozen_token);
402  REQUIRE(node_data.is_sorted());
403  THEN("New nodes cannot be created") {
404  // Underlying node data is read-only, cannot allocate new Nodes.
405  REQUIRE_THROWS(::Node{});
406  }
407  // The token enforces that values cannot move in memory, but it
408  // does not mean that they cannot be read from and written to
409  THEN("Values in existing nodes can be modified") {
410  auto& node = nodes.front();
411  REQUIRE_NOTHROW(node.v());
412  REQUIRE_NOTHROW(node.v() += 42.0);
413  }
414  THEN("The sorted-ness flag cannot be modified") {
415  REQUIRE_THROWS(node_data.mark_as_unsorted());
416  AND_THEN("Attempts to do so fail") {
417  REQUIRE(node_data.is_sorted());
418  }
419  }
420  THEN(
421  "The storage *can* be permuted if the sorted token is transferred back to the "
422  "container") {
423  node_data.apply_reverse_permutation(std::move(perm_vector), frozen_token);
424  }
425  THEN("The storage cannot be permuted when a 2nd sorted token is used") {
426  // Checking one of the permuting operations should be enough
427  REQUIRE_THROWS(node_data.apply_reverse_permutation(std::move(perm_vector)));
428  }
429  // In read-only mode we cannot delete Nodes either, but because
430  // we cannot throw from destructors it is not easy to test this
431  // in this context. There is a separate test for this below
432  // that is tagged with [tests_that_abort].
433  }
434  // sorted_token out of scope, underlying data no longer read-only
435  THEN("After the token is discarded, new Nodes can be allocated") {
436  REQUIRE_NOTHROW(::Node{});
437  }
438  }
439  }
440  REQUIRE(neuron::model().node_data().size() == 0);
441 }
442 
443 TEST_CASE("Fast membrane current storage", "[Neuron][data_structures][node][fast_imem]") {
444  REQUIRE(neuron::model().node_data().size() == 0);
445 
446  auto const set_fast_imem = [](bool new_value) {
447  nrn_use_fast_imem = new_value;
449  };
450  auto const check_throws = [](auto& node) {
451  THEN("fast_imem fields cannot be accessed") {
452  CHECK_THROWS(node.sav_d());
453  CHECK_THROWS(node.sav_rhs());
454  CHECK_FALSE(node.sav_rhs_handle());
455  }
456  };
457  auto const check_default = [](auto& node) {
458  THEN("fast_imem fields have their default values") {
459  CHECK(node.sav_d() == 0.0);
460  CHECK(node.sav_rhs() == 0.0);
461  CHECK(*node.sav_rhs_handle() == 0.0);
462  }
463  };
464  GIVEN("fast_imem calculation is disabled") {
465  set_fast_imem(false);
466  WHEN("A node is default-constructed") {
467  REQUIRE(neuron::model().node_data().size() == 0);
468  ::Node node{};
469  check_throws(node);
470  auto handle = node.sav_rhs_handle();
471  // The sav_rhs field is disabled, so the handle is a plain, completely null one.
472  CHECK(to_str(handle) == "data_handle<double>{null}");
474  "generic_data_handle{raw=nullptr type=double*}");
475  AND_WHEN("fast_imem calculation is enabled with a Node active") {
476  set_fast_imem(true);
477  check_default(node);
478  // The current implementation prefers simplicity to magic where possible, so handle
479  // will still be null.
480  CHECK_FALSE(handle);
481  }
482  }
483  }
484  GIVEN("fast_imem calculation is enabled") {
485  set_fast_imem(true);
486  WHEN("A node is default-constructed") {
487  REQUIRE(neuron::model().node_data().size() == 0);
488  ::Node node{};
489  check_default(node);
490  auto handle = node.sav_rhs_handle();
491  *handle = 42; // non-default value
492  generic_data_handle generic{handle};
493  CHECK(handle);
494  CHECK(to_str(handle) ==
495  "data_handle<double>{Node::field::FastIMemSavRHS row=0/1 val=42}");
496  CHECK(to_str(generic) ==
497  "generic_data_handle{Node::field::FastIMemSavRHS row=0/1 type=double*}");
498  AND_WHEN("fast_imem calculation is disabled with a Node active") {
499  REQUIRE(neuron::model().node_data().size() == 1);
500  set_fast_imem(false);
501  check_throws(node);
502  // This handle used to be valid, but it is now invalid because the optional field it
503  // refers to has been disabled.
504  CHECK_FALSE(handle);
505  CHECK(to_str(handle) == "data_handle<double>{cont=deleted row=0/unknown}");
506  CHECK(to_str(generic) ==
507  "generic_data_handle{cont=deleted row=0/unknown type=double*}");
508  AND_WHEN("fast_imem calculation is re-enabled") {
509  set_fast_imem(true);
510  // non-default value written above has been lost
511  check_default(node);
512  // Implementation choice was to minimise magic, so the handles are still dead
513  CHECK_FALSE(handle);
514  CHECK(to_str(handle) == "data_handle<double>{cont=deleted row=0/unknown}");
515  CHECK(to_str(generic) ==
516  "generic_data_handle{cont=deleted row=0/unknown type=double*}");
517  }
518  }
519  }
520  WHEN("A series of Nodes are created with non-trivial fast_imem values") {
521  constexpr auto num_nodes = 10;
522  std::vector<::Node> nodes(num_nodes);
523  std::vector<std::size_t> perm_vector(num_nodes);
524  for (auto i = 0; i < num_nodes; ++i) {
525  perm_vector[i] = i;
526  nodes[i].sav_d() = i * i;
527  nodes[i].sav_rhs() = i * i * i;
528  }
529  AND_WHEN("A random permutation is applied") {
530  std::mt19937 g{42};
531  std::shuffle(perm_vector.begin(), perm_vector.end(), g);
532  auto& node_data = neuron::model().node_data();
533  node_data.apply_reverse_permutation(std::move(perm_vector));
534  THEN("The logical values should still match") {
535  for (auto i = 0; i < num_nodes; ++i) {
536  REQUIRE(nodes[i].sav_d() == i * i);
537  REQUIRE(nodes[i].sav_rhs() == i * i * i);
538  }
539  }
540  }
541  }
542  }
543 }
544 
545 // Tests that cover code paths reaching std::terminate. "[.]" means they will not run by default,
546 // [tests_that_abort] means we have a tag to run them with.
547 TEST_CASE("Deleting a row from a frozen SoA container causes a fatal error",
548  "[.][tests_that_abort]") {
549  auto& node_data = neuron::model().node_data(); // SoA data store
550  std::optional<::Node> node{std::in_place}; // take ownership of a row in node_data
551  REQUIRE(node_data.size() == 1); // quick sanity check
552  auto const frozen_token = node_data.issue_frozen_token(); // mark node_data frozen
553  node.reset(); // Node destructor will trigger a call to std::terminate.
554 }
#define v
Definition: md1redef.h:11
#define i
Definition: md1redef.h:19
#define assert(ex)
Definition: hocassrt.h:24
static double map(void *v)
Definition: mlinedit.cpp:43
void move(Item *q1, Item *q2, Item *q3)
Definition: list.cpp:200
Item * next(Item *item)
Definition: list.cpp:89
void nrn_fast_imem_alloc()
Definition: fast_imem.cpp:32
bool nrn_use_fast_imem
Definition: fast_imem.cpp:19
handle_interface< non_owning_identifier< storage > > handle
Non-owning handle to a Node.
Definition: node_data.hpp:26
std::tuple< std::vector<::Node >, std::vector< double > > get_nodes_and_reference_voltages(std::size_t num_nodes=10)
Definition: node.cpp:243
std::vector< double > get_node_voltages(std::vector<::Node > const &nodes)
Definition: node.cpp:236
Model & model()
Access the global Model instance.
Definition: model_data.hpp:206
static std::string to_str(T const &x)
Definition: node.cpp:46
data_handle< T > transform(data_handle< T > handle, Transform type)
Definition: node.cpp:32
constexpr static double magic_voltage_value
Definition: node.cpp:43
Transform
Definition: node.cpp:30
@ ViaGenericDataHandle
TEST_CASE("data_handle<double>", "[Neuron][data_structures][data_handle]")
Definition: node.cpp:52
static Node * node(Object *)
Definition: netcvode.cpp:291
short type
Definition: cabvars.h:10
#define CHECK(name)
Definition: init.cpp:97
Definition: section.h:105
auto & sav_d()
Definition: section.h:165
auto & sav_rhs()
Definition: section.h:171
auto v_handle()
Definition: section.h:153
auto & area()
Definition: section.h:120
auto & v()
Definition: section.h:141
auto sav_rhs_handle()
Definition: section.h:177
auto non_owning_handle()
Definition: section.h:180
container::Node::storage & node_data()
Access the structure containing the data of all Nodes.
Definition: model_data.hpp:24
Area in um^2 but see treeset.cpp.
Definition: node.hpp:24
Base class defining the public API of Node handles.
Definition: node.hpp:90
field::Area::type & area()
Return the area.
Definition: node.hpp:109
field::Voltage::type & v()
Return the membrane potential.
Definition: node.hpp:172
Explicit specialisation data_handle<void>.
Stable handle to a generic value.
Definition: data_handle.hpp:61
Non-template stable handle to a generic value.
std::size_t current_row() const
Return current offset in the underlying storage where this object lives.
Definition: view_utils.hpp:44
frozen_token_type issue_frozen_token()
Create a token guaranteeing the container is in "frozen" state.
frozen_token_type apply_reverse_permutation(Arg &&permutation)
Permute the SoA-format data using an arbitrary range of integers.