C(++)ECCO
C++ Error Control COding: a header-only library for ECC simulations and experiments, modeling complete coding systems across arbitrary finite fields and complex inter-field relationships - Christian Senger <senger@inue.uni-stuttgart.de>
Loading...
Searching...
No Matches
helpers.hpp
Go to the documentation of this file.
1/**
2 * @file helpers.hpp
3 * @brief Utility functions and mathematical helpers
4 * @author Christian Senger <senger@inue.uni-stuttgart.de>
5 * @version 2.1.1
6 * @date 2026
7 *
8 * @copyright
9 * Copyright (c) 2026, Christian Senger <senger@inue.uni-stuttgart.de>
10 *
11 * Licensed for noncommercial use only, including academic teaching, research, and personal non-profit purposes.
12 * Commercial use is prohibited without a separate commercial license. See the [LICENSE](../../LICENSE) file in the
13 * repository root for full terms and how to request a commercial license.
14 *
15 * @section Description
16 *
17 * This header collects small utilities used by the algebraic types: random number generation,
18 * integer arithmetic, square-and-multiply exponentiation, double-and-add multiplication,
19 * caching, maxima, constexpr floor, and divisibility tests.
20 */
21
22#ifndef HELPERS_HPP
23#define HELPERS_HPP
24
25#include <algorithm>
26#include <atomic>
27#include <cmath>
28#include <cstdint>
29#include <functional>
30#include <limits>
31#include <mutex>
32#include <new>
33#include <optional>
34#include <random>
35#include <stdexcept>
36#include <string>
37#include <thread>
38#include <tuple>
39#include <type_traits>
40#include <utility>
41#include <vector>
42
43#include "InfInt.hpp"
44
45namespace CECCO {
46
47/**
48 * @brief Thread-local random number generator with shared seeding policy
49 *
50 * Provides one `std::mt19937` engine per thread. `seed()` selects deterministic seeding;
51 * `use_hardware_seed()` returns to seeding from `std::random_device`. A seed change is observed
52 * on the next call to @ref get in each thread.
53 *
54 * @note Use the static interface; the class has no object state.
55 */
56class RNG {
57 public:
58 /**
59 * @brief Get thread-local random number generator
60 * @return Reference to thread-local std::mt19937 generator
61 *
62 * Initializes the engine on first access. If the global seed generation changed since
63 * the previous access in this thread, reseeds before returning.
64 */
65 static std::mt19937& get() {
66 static thread_local std::mt19937 generator{get_initial_seed()};
67 if (should_reseed()) generator.seed(get_current_seed());
68 return generator;
69 }
70
71 /**
72 * @brief Set deterministic seed for all threads
73 * @param seed Base seed value (combined with thread ID)
74 *
75 * Selects deterministic seeding. Each thread uses @p seed XOR its thread ID hash.
76 */
77 static void seed(uint32_t seed) {
78 use_deterministic_seed.store(true);
79 deterministic_seed_value.store(seed);
80 reseed_generation.fetch_add(1); // Signal all threads to reseed
81 }
82
83 /**
84 * @brief Enable hardware-based random seeding
85 *
86 * Selects seeding from `std::random_device`.
87 */
88 static void use_hardware_seed() {
89 use_deterministic_seed.store(false);
90 reseed_generation.fetch_add(1); // Signal all threads to reseed
91 }
92
93 private:
94 static inline std::atomic<bool> use_deterministic_seed{false};
95 static inline std::atomic<uint32_t> deterministic_seed_value{0};
96 static inline std::atomic<uint32_t> reseed_generation{0};
97
98 static uint32_t get_initial_seed() {
99 if (use_deterministic_seed.load()) {
100 auto tid = std::hash<std::thread::id>{}(std::this_thread::get_id());
101 return deterministic_seed_value.load() ^ static_cast<uint32_t>(tid);
102 } else {
103 static thread_local std::random_device rd;
104 return rd();
105 }
106 }
107
108 static uint32_t get_current_seed() { return get_initial_seed(); }
109
110 static bool should_reseed() {
111 static thread_local uint32_t last_seen_generation = 0;
112 uint32_t current_generation = reseed_generation.load();
113 if (current_generation != last_seen_generation) {
114 last_seen_generation = current_generation;
115 return true;
116 }
117 return false;
118 }
119};
120
121/**
122 * @brief Current thread's random number generator
123 * @return Reference to thread-local random number generator
124 *
125 * Equivalent to @ref RNG::get.
126 */
127inline std::mt19937& gen() { return RNG::get(); }
128
129/**
130 * @brief Indices of all maximum elements
131 * @tparam T Element type (must support operator< for comparison)
132 * @param v Input vector
133 * @return Indices i with `v[i] == max(v)`; empty if @p v is empty
134 *
135 * Uses two passes over @p v.
136 */
137template <class T>
139 std::vector<size_t> indices;
140 if (v.empty()) return indices;
141 const auto max_value = *std::max_element(v.begin(), v.end());
142 for (size_t i = 0; i < v.size(); ++i) {
143 if (v[i] == max_value) indices.push_back(i);
144 }
145 return indices;
146}
147
148/**
149 * @brief Primality test by trial division
150 * @tparam T Unsigned integer type
151 * @param a Number to test for primality
152 * @return true if a is prime, false otherwise
153 *
154 * Tests odd divisors up to √a. Returns false for a ≤ 1 and even a > 2.
155 */
156template <class T>
157constexpr bool is_prime(T a) noexcept {
158 if (a == 2) return true;
159 if (a <= 1 || !(a & 1)) return false;
160 // find "smaller half" of factorization (if factor > sqrt(a) there must be a factor < sqrt(a))
161 for (T b = 3; b * b <= a; b += 2)
162 if ((a % b) == 0) return false;
163 return true;
164}
165
166/**
167 * @brief Greatest common divisor and optional Bézout coefficients
168 * @tparam T Signed integral type
169 * @param a First integer
170 * @param b Second integer
171 * @param s Pointer to store Bézout coefficient for a (optional)
172 * @param t Pointer to store Bézout coefficient for b (optional)
173 * @return Greatest common divisor of a and b
174 *
175 * If @p s and @p t are non-null, stores coefficients satisfying `a*s + b*t = gcd(a,b)`.
176 */
177template <class T>
178constexpr T GCD(T a, T b, T* s = nullptr, T* t = nullptr) noexcept {
179 static_assert((std::is_integral_v<T> && std::is_signed_v<T>) || std::is_same_v<T, InfInt>,
180 "GCD requires signed integral type or InfInt");
181 if (s != nullptr && t != nullptr) { // extended EA
182 *s = T(1);
183 *t = T(0);
184 T u = T(0);
185 T v = T(1);
186 while (b != T(0)) {
187 const T q = a / b;
188 T b1 = std::move(b);
189 b = a - q * b1;
190 a = std::move(b1);
191 T u1 = std::move(u);
192 u = *s - q * u1;
193 *s = std::move(u1);
194 T v1 = std::move(v);
195 v = *t - q * v1;
196 *t = std::move(v1);
197 }
198 } else { // "normal" EA
199 while (b != T(0)) {
200 const T q = a / b;
201 T b1 = std::move(b);
202 b = a - q * b1;
203 a = std::move(b1);
204 }
205 }
206 return a;
207}
208
209/**
210 * @brief Multiplicative inverse modulo a prime
211 * @tparam p Prime modulus (must be prime for correct results)
212 * @tparam T Signed integer type
213 * @param a Element to invert
214 * @return Modular inverse a^(-1) mod p
215 *
216 * Uses the extended Euclidean algorithm.
217 */
218template <uint16_t p, class T>
219constexpr T modinv(T a) noexcept {
220 static_assert(is_prime(p), "p is not a prime");
221 T s, t;
222 GCD<T>(std::move(a), T(p), &s, &t); // don't actually need the gcd
223 T result = s % T(p);
224 return result < 0 ? result + T(p) : result;
225}
226
227/**
228 * @brief Factorial a!
229 * @tparam T Integer type
230 * @param a Non-negative integer
231 * @return `a! = a * (a-1) * ... * 2 * 1`
232 */
233template <class T>
234T fac(T a) noexcept {
235 T res = 1;
236 while (a > 1) {
237 res *= a;
238 --a;
239 }
240 return res;
241}
242
243/**
244 * @brief Binomial coefficient C(n,k)
245 * @tparam T Integer type
246 * @param n Total number of items
247 * @param k Number of items to choose
248 * @return `C(n,k) = n! / (k! * (n-k)!)`
249 *
250 * Uses the multiplicative formula and the symmetry `C(n,k) = C(n,n-k)`.
251 */
252template <class T>
253T bin(const T& n, T k) noexcept {
254 if (k > n) return 0;
255 if (k == 0 || n == k) return 1;
256 if (k > n - k) k = n - k; // symmetry
257 T res = 1;
258 for (T i = 1; i <= k; ++i) res = res * (n - k + i) / i;
259 return res;
260}
261
262/**
263 * @brief Binomial coefficient specialization for @ref InfInt
264 * @param n Total number of items
265 * @param k Number of items to choose
266 * @return `C(n,k)` as an @ref InfInt
267 *
268 * Builds numerator and denominator separately, then performs one division.
269 */
270template <>
271inline InfInt bin(const InfInt& n, InfInt k) noexcept {
272 if (k > n) return 0;
273 if (k == 0 || n == k) return 1;
274 if (n == 0) return 0;
275 if (k > n - k) k = n - k; // symmetry
276 InfInt numerator = 1;
277 InfInt denominator = 1;
278 for (InfInt i = 1; i <= k; ++i) {
279 numerator *= n + 1 - i;
280 denominator *= i;
281 }
282 return numerator / denominator;
283}
284
285/**
286 * @brief Exponentiation by square-and-multiply
287 * @tparam T Type supporting multiplication and, for negative exponents, division
288 * @param b Base value
289 * @param e Exponent
290 * @return `b^e`
291 *
292 * For negative exponents, computes `(1/b)^|e|`.
293 *
294 * @note Returns `T(1)` for e = 0.
295 */
296template <class T>
297constexpr T sqm(T b, int e) {
298 static_assert(std::is_integral_v<decltype(e)>, "exponent must be integral type");
299 if (e == 0) return T(1);
300 if (e < 0) {
301 b = T(1) / b;
302 if (e == std::numeric_limits<int>::min())
303 throw std::invalid_argument(
304 "Exponent e too large!"); // INT_MIN might be INT_MAX+1, potential problem in next line
305 e = -e;
306 }
307 // square and multiply
308 T temp(1);
309 unsigned int exp = static_cast<unsigned int>(e);
310 while (exp > 0) {
311 if (exp & 1) temp *= b;
312 b *= b;
313 exp >>= 1;
314 }
315 return temp;
316}
317
318/**
319 * @brief Scalar multiplication by double-and-add
320 * @tparam T Type supporting addition and unary minus
321 * @param b Multiplicand
322 * @param m Integer multiplier
323 * @return `b * m`
324 *
325 * For negative multipliers, computes `(-b) * |m|`.
326 *
327 * @note Returns `T(0)` for m = 0.
328 */
329template <class T>
330constexpr T daa(T b, int m) {
331 static_assert(std::is_integral_v<decltype(m)>, "multiplicand must be integral type");
332 if (m == 0) return T(0);
333 if (m < 0) {
334 b = -b;
335 if (m == std::numeric_limits<int>::min())
336 throw std::invalid_argument(
337 "Multiplier m too large!"); // INT_MIN might be INT_MAX+1, potential problem in next line
338 m = -m;
339 }
340 // double and add
341 T temp(0);
342 unsigned int um = static_cast<unsigned int>(m);
343 while (um > 0) {
344 if (um & 1) temp += b;
345 b += b;
346 um >>= 1;
347 }
348 return temp;
349}
350
351namespace details {
352
353/**
354 * @brief Constexpr floor function
355 * @param x Floating-point value
356 * @return Floor of x (largest integer ≤ x)
357 *
358 * Alternative to `std::floor` for constant evaluation.
359 *
360 * @note Returns `double`, matching `std::floor`.
361 */
362constexpr double floor_constexpr(double x) {
363 long int i = static_cast<long int>(x);
364 return (x < 0 && x != i) ? i - 1 : i;
365}
366
367/**
368 * @brief Cache entry specification
369 * @tparam ID Entry identifier
370 * @tparam T Stored value type
371 *
372 * Associates an entry ID with its value type for @ref Cache.
373 *
374 * @note IDs must be unique within one @ref Cache instance.
375 */
376template <auto ID, typename T>
378 static constexpr auto id = ID;
379 using type = T;
380};
381
382/**
383 * @brief Heterogeneous cache indexed by entry ID
384 *
385 * @tparam ENTRIES Pack of @ref CacheEntry types
386 *
387 * Each ID gets its own `std::optional<T>` slot in a `std::tuple`, so different IDs may share
388 * the same value type without ambiguity. Lookup is by compile-time ID; an unknown ID is a
389 * compile-time error.
390 *
391 * @warning Not thread-safe for concurrent writes. Use external synchronisation if multiple
392 * threads may call `set()`, `invalidate()`, or `operator()` simultaneously.
393 *
394 * @section Usage_Example
395 *
396 * @code{.cpp}
397 * using Entry1 = CacheEntry<0, std::vector<int>>;
398 * using Entry2 = CacheEntry<1, double>;
399 * using Entry3 = CacheEntry<5, std::string>; // IDs need not be consecutive
400 * Cache<Entry1, Entry2, Entry3> cache;
401 *
402 * cache.set<0>(std::vector<int>{1, 2, 3});
403 * auto& vec = cache.get_or_compute<0>([] { return std::vector<int>{4, 5, 6}; });
404 * if (cache.is_set<0>()) cache.invalidate<0>();
405 * @endcode
406 */
407template <typename... ENTRIES>
408class Cache {
409 private:
410 // ID -> position in the ENTRIES pack. Two constrained partial specialisations select
411 // lazily, so the recursion only instantiates the not-yet-matched tail.
412 template <auto ID, size_t I, typename...>
413 struct index_finder;
414
415 template <auto ID, size_t I, typename First, typename... Rest>
416 requires(First::id == ID)
417 struct index_finder<ID, I, First, Rest...> {
418 static constexpr size_t value = I;
419 };
420
421 template <auto ID, size_t I, typename First, typename... Rest>
422 requires(First::id != ID)
423 struct index_finder<ID, I, First, Rest...> {
424 static constexpr size_t value = index_finder<ID, I + 1, Rest...>::value;
425 };
426
427 template <auto ID>
428 static constexpr size_t index_for = index_finder<ID, 0, ENTRIES...>::value;
429
430 template <auto ID>
431 using type_for = typename std::tuple_element_t<index_for<ID>, std::tuple<ENTRIES...>>::type;
432
433 // One slot per entry, indexed by position. Independent slots avoid the duplicate-types
434 // ambiguity that a `std::variant<monostate, T1, T2, …>` would have when two entries
435 // happen to share the same value type.
436 mutable std::tuple<std::optional<typename ENTRIES::type>...> slots{};
437
438 public:
439 Cache() = default;
440
441 template <auto ID>
442 bool is_set() const noexcept {
443 return std::get<index_for<ID>>(slots).has_value();
444 }
445
446 template <auto ID, typename TYPE>
447 void set(TYPE&& value) const {
448 std::get<index_for<ID>>(slots) = static_cast<type_for<ID>>(std::forward<TYPE>(value));
449 }
450
451 template <auto ID>
452 bool invalidate() const noexcept {
453 auto& slot = std::get<index_for<ID>>(slots);
454 const bool was_set = slot.has_value();
455 slot.reset();
456 return was_set;
457 }
458
459 bool invalidate() const noexcept {
460 return std::apply(
461 [](auto&... s) {
462 const bool any = (s.has_value() || ...);
463 (s.reset(), ...);
464 return any;
465 },
466 slots);
467 }
468
469 template <auto ID>
470 const type_for<ID>& operator()(auto&& calculate_func) const {
471 auto& slot = std::get<index_for<ID>>(slots);
472 if (!slot.has_value()) slot = calculate_func();
473 return *slot;
474 }
475
476 template <auto ID>
478 return std::get<index_for<ID>>(slots);
479 }
480
481 template <auto ID>
482 const type_for<ID>& get_or_compute(auto&& calculate_func) const {
483 return operator()<ID>(std::forward<decltype(calculate_func)>(calculate_func));
484 }
485};
486
487/**
488 * @brief Thread-safe single-value cache
489 * @tparam T Value type stored in the cache
490 *
491 * Stores one optional value together with a `std::once_flag`. Use `call_once()` to guard
492 * lazy initialization when multiple threads may read the same object. Copying or moving a
493 * cache copies or moves the stored value, if any, and creates a fresh once flag.
494 *
495 * @warning Concurrent reads through `call_once()` are thread-safe. Assignment, `emplace()`, and
496 * manual value changes are not synchronization points and must not race with other operations.
497 *
498 * @section Usage_Example
499 *
500 * @code{.cpp}
501 * mutable OnceCache<size_t> weight;
502 *
503 * weight.call_once([this] {
504 * if (weight.has_value()) return;
505 * weight.emplace(calculate_weight());
506 * });
507 *
508 * return weight.value();
509 * @endcode
510 */
511template <class T>
513 public:
514 OnceCache() = default;
515
516 OnceCache(const OnceCache& other) {
517 if (other.has_value()) data.emplace(other.value());
518 }
519
521 if (other.has_value()) data.emplace(std::move(other.value()));
522 }
523
524 OnceCache& operator=(const OnceCache& other) {
525 if (this != &other) {
526 reset_for_assignment();
527 if (other.has_value()) data.emplace(other.value());
528 }
529 return *this;
530 }
531
533 if (this != &other) {
534 reset_for_assignment();
535 if (other.has_value()) data.emplace(std::move(other.value()));
536 }
537 return *this;
538 }
539
540 OnceCache& operator=(const T& value) {
541 data = value;
542 return *this;
543 }
544
545 OnceCache& operator=(T&& value) {
546 data = std::move(value);
547 return *this;
548 }
549
550 template <class F>
551 void call_once(F&& f) const {
552 std::call_once(flag, std::forward<F>(f));
553 }
554
555 template <class... Args>
556 T& emplace(Args&&... args) const {
557 return data.emplace(std::forward<Args>(args)...);
558 }
559
560 bool has_value() const noexcept { return data.has_value(); }
561
562 explicit operator bool() const noexcept { return has_value(); }
563
564 T& value() { return data.value(); }
565
566 const T& value() const { return data.value(); }
567
568 T& operator*() { return *data; }
569
570 const T& operator*() const { return *data; }
571
572 T* operator->() { return &*data; }
573
574 const T* operator->() const { return &*data; }
575
576 void reset() const { data.reset(); }
577
578 private:
579 void reset_for_assignment() {
580 data.reset();
581 flag.~once_flag();
582 new (&flag) std::once_flag();
583 }
584
585 mutable std::optional<T> data;
586 mutable std::once_flag flag;
587};
588
589inline std::string basename(const char* path) {
590 std::string s(path);
591
592 const auto pos = s.find_last_of("/\\");
593 if (pos != std::string::npos) s.erase(0, pos + 1);
594
595 const auto dot = s.find_last_of('.');
596 if (dot != std::string::npos && dot != 0) s.erase(dot);
597
598 return s;
599}
600
601static const uint8_t colormap[64][3] = {
602 {0, 0, 0}, {0, 0, 24}, {0, 0, 40}, {0, 0, 56}, {0, 0, 72}, {0, 0, 88},
603 {0, 0, 104}, {0, 0, 120}, {0, 0, 136}, {0, 0, 152}, {0, 0, 167}, {0, 0, 183},
604 {0, 0, 199}, {0, 0, 215}, {0, 0, 231}, {0, 0, 252}, {0, 6, 253}, {0, 24, 232},
605 {0, 40, 216}, {0, 56, 200}, {0, 72, 184}, {0, 88, 168}, {0, 104, 152}, {0, 120, 136},
606 {0, 136, 120}, {0, 152, 104}, {0, 167, 88}, {0, 183, 72}, {0, 199, 56}, {0, 215, 41},
607 {0, 231, 25}, {0, 249, 6}, {6, 255, 0}, {24, 255, 0}, {40, 255, 0}, {56, 255, 0},
608 {72, 255, 0}, {88, 255, 0}, {104, 255, 0}, {120, 255, 0}, {136, 255, 0}, {152, 255, 0},
609 {167, 255, 0}, {183, 255, 0}, {199, 255, 0}, {215, 255, 0}, {231, 255, 0}, {249, 255, 0},
610 {255, 255, 6}, {255, 255, 24}, {255, 255, 40}, {255, 255, 56}, {255, 255, 72}, {255, 255, 88},
611 {255, 255, 104}, {255, 255, 120}, {255, 255, 136}, {255, 255, 152}, {255, 255, 167}, {255, 255, 183},
612 {255, 255, 199}, {255, 255, 215}, {255, 255, 231}, {255, 255, 255}};
613
614} // namespace details
615
616} // namespace CECCO
617
618#endif
Thread-local random number generator with shared seeding policy.
Definition helpers.hpp:56
static void seed(uint32_t seed)
Set deterministic seed for all threads.
Definition helpers.hpp:77
static void use_hardware_seed()
Enable hardware-based random seeding.
Definition helpers.hpp:88
static std::mt19937 & get()
Get thread-local random number generator.
Definition helpers.hpp:65
Heterogeneous cache indexed by entry ID.
Definition helpers.hpp:408
std::optional< type_for< ID > > get() const
Definition helpers.hpp:477
bool invalidate() const noexcept
Definition helpers.hpp:459
bool is_set() const noexcept
Definition helpers.hpp:442
bool invalidate() const noexcept
Definition helpers.hpp:452
const type_for< ID > & get_or_compute(auto &&calculate_func) const
Definition helpers.hpp:482
void set(TYPE &&value) const
Definition helpers.hpp:447
const type_for< ID > & operator()(auto &&calculate_func) const
Definition helpers.hpp:470
Thread-safe single-value cache.
Definition helpers.hpp:512
OnceCache & operator=(const OnceCache &other)
Definition helpers.hpp:524
void call_once(F &&f) const
Definition helpers.hpp:551
const T & operator*() const
Definition helpers.hpp:570
const T * operator->() const
Definition helpers.hpp:574
OnceCache & operator=(const T &value)
Definition helpers.hpp:540
OnceCache(const OnceCache &other)
Definition helpers.hpp:516
T & emplace(Args &&... args) const
Definition helpers.hpp:556
OnceCache & operator=(T &&value)
Definition helpers.hpp:545
OnceCache(OnceCache &&other)
Definition helpers.hpp:520
operator bool() const noexcept
Definition helpers.hpp:562
bool has_value() const noexcept
Definition helpers.hpp:560
OnceCache & operator=(OnceCache &&other)
Definition helpers.hpp:532
const T & value() const
Definition helpers.hpp:566
bool operator<=(const InfInt &rhs) const
Definition InfInt.hpp:743
bool operator>(const InfInt &rhs) const
Definition InfInt.hpp:768
const InfInt & operator*=(const InfInt &rhs)
Definition InfInt.hpp:492
const InfInt & operator=(const InfInt &l)
Definition InfInt.hpp:428
const InfInt & operator++()
Definition InfInt.hpp:436
InfInt operator-(const InfInt &rhs) const
Definition InfInt.hpp:583
InfInt operator/(const InfInt &rhs) const
Definition InfInt.hpp:632
bool operator==(const InfInt &rhs) const
Definition InfInt.hpp:692
Contains implementation details not to be exposed to the user. Functions and classes here may change ...
Definition blocks.hpp:59
std::string basename(const char *path)
Definition helpers.hpp:589
static const uint8_t colormap[64][3]
Definition helpers.hpp:601
constexpr double floor_constexpr(double x)
Constexpr floor function.
Definition helpers.hpp:362
Provides a framework for error correcting codes.
Definition blocks.hpp:57
std::mt19937 & gen()
Current thread's random number generator.
Definition helpers.hpp:127
std::vector< size_t > find_maxima(const std::vector< T > &v)
Indices of all maximum elements.
Definition helpers.hpp:138
constexpr T GCD(T a, T b, T *s=nullptr, T *t=nullptr) noexcept
Greatest common divisor and optional Bézout coefficients.
Definition helpers.hpp:178
constexpr T daa(T b, int m)
Scalar multiplication by double-and-add.
Definition helpers.hpp:330
InfInt bin(const InfInt &n, InfInt k) noexcept
Binomial coefficient specialization for InfInt.
Definition helpers.hpp:271
constexpr T modinv(T a) noexcept
Multiplicative inverse modulo a prime.
Definition helpers.hpp:219
constexpr T sqm(T b, int e)
Exponentiation by square-and-multiply.
Definition helpers.hpp:297
constexpr bool is_prime(T a) noexcept
Primality test by trial division.
Definition helpers.hpp:157
T fac(T a) noexcept
Factorial a!
Definition helpers.hpp:234
T bin(const T &n, T k) noexcept
Binomial coefficient C(n,k).
Definition helpers.hpp:253
Cache entry specification.
Definition helpers.hpp:377
static constexpr auto id
Definition helpers.hpp:378