Line data Source code
1 0 : // Distributed under the MIT License.
2 : // See LICENSE.txt for details.
3 :
4 : #pragma once
5 :
6 : #include <array>
7 : #include <cstddef>
8 : #include <utility>
9 :
10 : #include "Utilities/ErrorHandling/Assert.hpp"
11 : #include "Utilities/Gsl.hpp"
12 : #include "Utilities/Requires.hpp"
13 : #include "Utilities/TMPL.hpp"
14 : #include "Utilities/TypeTraits/IsInteger.hpp"
15 :
16 : /// \ingroup UtilitiesGroup
17 : /// Range of integral values for StaticCache indices. The `Start` is inclusive
18 : /// and the `End` is exclusive. The range must not be empty.
19 : template <auto Start, auto End>
20 1 : struct CacheRange {
21 : static_assert(std::is_same_v<decltype(Start), decltype(End)>);
22 : static_assert(Start < End, "CacheRange must include at least one value");
23 0 : constexpr static auto start = Start;
24 0 : constexpr static auto end = End;
25 0 : constexpr static auto size = end - start;
26 0 : using value_type = std::remove_cv_t<decltype(start)>;
27 : };
28 :
29 : /// \ingroup UtilitiesGroup
30 : /// Possible enumeration values for the StaticCache. Only values specified here
31 : /// are retrievable.
32 : ///
33 : /// \note The `EnumerationType` must be streamable.
34 : template <typename EnumerationType, EnumerationType... Enums>
35 1 : struct CacheEnumeration {
36 0 : constexpr static size_t size = sizeof...(Enums);
37 0 : using value_type = EnumerationType;
38 0 : static constexpr std::array<value_type, size> values{Enums...};
39 0 : using value_list = tmpl::integral_list<EnumerationType, Enums...>;
40 : };
41 :
42 : /// \ingroup UtilitiesGroup
43 : /// A cache of objects intended to be stored in a static variable.
44 : ///
45 : /// Objects can be accessed via a combination of several `size_t` and `enum`
46 : /// arguments. The range of each integral argument is specified via a template
47 : /// parameter of type `CacheRange<start, end>`, giving the first and
48 : /// one-past-last values for the range. Each `enum` argument is specified by a
49 : /// template parameter of type `CacheEnumeration<EnumerationType, Members...>`
50 : /// giving the enumeration type and an explicit set of every enum member to be
51 : /// cached.
52 : ///
53 : /// \example
54 : /// A cache with only numeric indices:
55 : /// \snippet Test_StaticCache.cpp static_cache
56 : ///
57 : /// \example
58 : /// A cache with enumeration indices:
59 : /// \snippet Test_StaticCache.cpp static_cache_with_enum
60 : ///
61 : /// \example
62 : /// A cache with mixed numeric and enumeration indices:
63 : /// \snippet Test_StaticCache.cpp static_cache_with_enum_and_numeric
64 : ///
65 : /// \example
66 : /// A cache with no arguments at all (caching only a single object)
67 : /// \snippet Test_StaticCache.cpp static_cache_no_args
68 : ///
69 : /// \see make_static_cache
70 : ///
71 : /// \tparam T type held in the cache
72 : /// \tparam Ranges ranges of valid indices
73 : template <typename Generator, typename T, typename... Ranges>
74 1 : class StaticCache {
75 : public:
76 : template <typename Gen>
77 : // NOLINTNEXTLINE(bugprone-forwarding-reference-overload)
78 0 : explicit StaticCache(Gen&& generator)
79 : : generator_{std::forward<Gen>(generator)} {}
80 :
81 : template <typename... Args>
82 0 : const T& operator()(const Args... parameters) const {
83 : static_assert(sizeof...(parameters) == sizeof...(Ranges),
84 : "Number of arguments must match number of ranges.");
85 : return unwrap_cache_combined(generate_tuple<Ranges>(parameters)...);
86 : }
87 :
88 : private:
89 : template <typename Range, typename T1>
90 0 : auto generate_tuple(const T1 parameter) const {
91 : if constexpr (std::is_enum<T1>::value) {
92 : static_assert(
93 : std::is_same<typename Range::value_type, std::remove_cv_t<T1>>::value,
94 : "Mismatched enum parameter type and cached type.");
95 : size_t array_location = std::numeric_limits<size_t>::max();
96 : static const std::array<typename Range::value_type, Range::size> values{
97 : Range::values};
98 : for (size_t i = 0; i < Range::size; ++i) {
99 : if (parameter == gsl::at(values, i)) {
100 : array_location = i;
101 : break;
102 : }
103 : }
104 : if (UNLIKELY(array_location == std::numeric_limits<size_t>::max())) {
105 : ERROR("Uncached enumeration value: " << parameter);
106 : }
107 : return std::tuple{array_location, typename Range::value_list{}};
108 : } else {
109 : static_assert(
110 : tt::is_integer_v<std::remove_cv_t<T1>>,
111 : "The parameter passed for a CacheRange must be an integer type.");
112 :
113 : // Check range here because the nested range checks in the unwrap_cache
114 : // function cause significant compile time overhead.
115 : if (UNLIKELY(Range::start >
116 : static_cast<decltype(Range::start)>(parameter) or
117 : static_cast<decltype(Range::start)>(parameter) >=
118 : Range::start +
119 : static_cast<decltype(Range::start)>(Range::size))) {
120 : ERROR("Index out of range: "
121 : << Range::start << " <= " << parameter << " < "
122 : << Range::start +
123 : static_cast<decltype(Range::start)>(Range::size));
124 : }
125 : return std::tuple{
126 : // unsigned cast is safe since this is an index into an array
127 : static_cast<size_t>(
128 : static_cast<typename Range::value_type>(parameter) -
129 : Range::start),
130 : tmpl::make_sequence<
131 : tmpl::integral_constant<typename Range::value_type, Range::start>,
132 : Range::size>{}};
133 : }
134 : }
135 :
136 : // Compilation time notes:
137 : //
138 : // - The separate peeling of different number of arguments is the
139 : // fastest implementation Nils Deppe has found so far.
140 : // - The second fastest is using a Cartesian product on the lists of
141 : // possible values, followed by a for_each over that list to set the
142 : // function pointers in the array. Note that having the for_each function
143 : // be marked `constexpr` resulted in a 6x reduction in compilation time
144 : // for clang 17 compared to the not constexpr version, but still 40%
145 : // slower compilation compared to the pattern matching below.
146 : template <typename... IntegralConstantValues>
147 0 : const T& unwrap_cache_combined() const {
148 : static const T cached_object = generator_(IntegralConstantValues::value...);
149 : return cached_object;
150 : }
151 :
152 : #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ > 10 && __GNUC__ < 14
153 : #pragma GCC diagnostic push
154 : #pragma GCC diagnostic ignored "-Warray-bounds"
155 : #endif
156 : template <typename... IntegralConstantValues, typename... IntegralConstants>
157 0 : const T& unwrap_cache_combined(
158 : std::tuple<size_t, tmpl::list<IntegralConstants...>> parameter0) const {
159 : // note that the act of assigning to the specified function pointer type
160 : // fixes the template arguments that need to be inferred.
161 : static const std::array<const T& (StaticCache::*)() const,
162 : sizeof...(IntegralConstants)>
163 : cache{{&StaticCache::unwrap_cache_combined<IntegralConstantValues...,
164 : IntegralConstants>...}};
165 : // The array `cache` holds pointers to member functions, so we dereference
166 : // the pointer and invoke it on `this`.
167 : return (this->*gsl::at(cache, std::get<0>(parameter0)))();
168 : }
169 :
170 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
171 : typename... IntegralConstants1>
172 0 : const T& unwrap_cache_combined(
173 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
174 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1) const {
175 : constexpr size_t num0 = sizeof...(IntegralConstants0);
176 : constexpr size_t num1 = sizeof...(IntegralConstants1);
177 : constexpr size_t total_size = num0 * num1;
178 : // note that the act of assigning to the specified function pointer type
179 : // fixes the template arguments that need to be inferred.
180 : static const std::array<const T& (StaticCache::*)() const, total_size>
181 : cache = []() {
182 : std::array<const T& (StaticCache::*)() const, total_size> result;
183 : size_t counter1 = 0;
184 : const auto helper1 = [&counter1,
185 : &result]<typename IntegralConstant1>() {
186 : size_t counter0 = 0;
187 : const auto helper0 = [&counter0, &counter1,
188 : &result]<typename IntegralConstant0>() {
189 : result[counter0 + num0 * counter1] =
190 : &StaticCache::unwrap_cache_combined<IntegralConstantValues...,
191 : IntegralConstant0,
192 : IntegralConstant1>;
193 : ++counter0;
194 : };
195 : EXPAND_PACK_LEFT_TO_RIGHT(
196 : helper0.template operator()<IntegralConstants0>());
197 : ++counter1;
198 : };
199 : EXPAND_PACK_LEFT_TO_RIGHT(
200 : helper1.template operator()<IntegralConstants1>());
201 : return result;
202 : }();
203 :
204 : // The array `cache` holds pointers to member functions, so we dereference
205 : // the pointer and invoke it on `this`.
206 : return (this->*gsl::at(cache, std::get<0>(parameter0) +
207 : num0 * std::get<0>(parameter1)))();
208 : }
209 :
210 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
211 : typename... IntegralConstants1, typename... IntegralConstants2>
212 0 : const T& unwrap_cache_combined(
213 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
214 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1,
215 : std::tuple<size_t, tmpl::list<IntegralConstants2...>> parameter2) const {
216 : constexpr size_t num0 = sizeof...(IntegralConstants0);
217 : constexpr size_t num1 = sizeof...(IntegralConstants1);
218 : constexpr size_t num2 = sizeof...(IntegralConstants2);
219 : constexpr size_t total_size = num0 * num1 * num2;
220 : // note that the act of assigning to the specified function pointer type
221 : // fixes the template arguments that need to be inferred.
222 : static const std::array<const T& (StaticCache::*)() const, total_size>
223 : cache = []() {
224 : std::array<const T& (StaticCache::*)() const, total_size> result;
225 : size_t counter2 = 0;
226 : const auto helper2 = [&counter2,
227 : &result]<typename IntegralConstant2>() {
228 : size_t counter1 = 0;
229 : const auto helper1 = [&counter1, &counter2,
230 : &result]<typename IntegralConstant1>() {
231 : size_t counter0 = 0;
232 : const auto helper0 = [&counter0, &counter1, &counter2,
233 : &result]<typename IntegralConstant0>() {
234 : result[counter0 + num0 * (counter1 + num1 * counter2)] =
235 : &StaticCache::unwrap_cache_combined<
236 : IntegralConstantValues..., IntegralConstant0,
237 : IntegralConstant1, IntegralConstant2>;
238 : ++counter0;
239 : };
240 : EXPAND_PACK_LEFT_TO_RIGHT(
241 : helper0.template operator()<IntegralConstants0>());
242 : ++counter1;
243 : };
244 : EXPAND_PACK_LEFT_TO_RIGHT(
245 : helper1.template operator()<IntegralConstants1>());
246 : ++counter2;
247 : };
248 : EXPAND_PACK_LEFT_TO_RIGHT(
249 : helper2.template operator()<IntegralConstants2>());
250 : return result;
251 : }();
252 :
253 : // The array `cache` holds pointers to member functions, so we dereference
254 : // the pointer and invoke it on `this`.
255 : return (
256 : this->*gsl::at(cache, std::get<0>(parameter0) +
257 : num0 * (std::get<0>(parameter1) +
258 : num1 * std::get<0>(parameter2))))();
259 : }
260 :
261 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
262 : typename... IntegralConstants1, typename... IntegralConstants2,
263 : typename... IntegralConstants3>
264 0 : const T& unwrap_cache_combined(
265 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
266 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1,
267 : std::tuple<size_t, tmpl::list<IntegralConstants2...>> parameter2,
268 : std::tuple<size_t, tmpl::list<IntegralConstants3...>> parameter3) const {
269 : constexpr size_t num0 = sizeof...(IntegralConstants0);
270 : constexpr size_t num1 = sizeof...(IntegralConstants1);
271 : constexpr size_t num2 = sizeof...(IntegralConstants2);
272 : constexpr size_t num3 = sizeof...(IntegralConstants3);
273 : constexpr size_t total_size = num0 * num1 * num2 * num3;
274 : // note that the act of assigning to the specified function pointer type
275 : // fixes the template arguments that need to be inferred.
276 : static const std::array<const T& (StaticCache::*)() const, total_size>
277 : cache = []() {
278 : std::array<const T& (StaticCache::*)() const, total_size> result;
279 : size_t counter3 = 0;
280 : const auto helper3 = [&counter3,
281 : &result]<typename IntegralConstant3>() {
282 : size_t counter2 = 0;
283 : const auto helper2 = [&counter2, &counter3,
284 : &result]<typename IntegralConstant2>() {
285 : size_t counter1 = 0;
286 : const auto helper1 = [&counter1, &counter2, &counter3,
287 : &result]<typename IntegralConstant1>() {
288 : size_t counter0 = 0;
289 : const auto helper0 = [&counter0, &counter1, &counter2,
290 : &counter3,
291 : &result]<typename IntegralConstant0>() {
292 : result[counter0 +
293 : num0 *
294 : (counter1 + num1 * (counter2 + num2 * counter3))] =
295 : &StaticCache::unwrap_cache_combined<
296 : IntegralConstantValues..., IntegralConstant0,
297 : IntegralConstant1, IntegralConstant2,
298 : IntegralConstant3>;
299 : ++counter0;
300 : };
301 : EXPAND_PACK_LEFT_TO_RIGHT(
302 : helper0.template operator()<IntegralConstants0>());
303 : ++counter1;
304 : };
305 : EXPAND_PACK_LEFT_TO_RIGHT(
306 : helper1.template operator()<IntegralConstants1>());
307 : ++counter2;
308 : };
309 : EXPAND_PACK_LEFT_TO_RIGHT(
310 : helper2.template operator()<IntegralConstants2>());
311 : ++counter3;
312 : };
313 : EXPAND_PACK_LEFT_TO_RIGHT(
314 : helper3.template operator()<IntegralConstants3>());
315 : return result;
316 : }();
317 :
318 : // The array `cache` holds pointers to member functions, so we
319 : // dereference the pointer and invoke it on `this`.
320 : return (
321 : this->*gsl::at(cache,
322 : std::get<0>(parameter0) +
323 : num0 * (std::get<0>(parameter1) +
324 : num1 * (std::get<0>(parameter2) +
325 : num2 * std::get<0>(parameter3)))))();
326 : }
327 :
328 : template <typename... IntegralConstantValues, typename... IntegralConstants0,
329 : typename... IntegralConstants1, typename... IntegralConstants2,
330 : typename... IntegralConstants3, typename... IntegralConstants4,
331 : typename... Args>
332 0 : const T& unwrap_cache_combined(
333 : std::tuple<size_t, tmpl::list<IntegralConstants0...>> parameter0,
334 : std::tuple<size_t, tmpl::list<IntegralConstants1...>> parameter1,
335 : std::tuple<size_t, tmpl::list<IntegralConstants2...>> parameter2,
336 : std::tuple<size_t, tmpl::list<IntegralConstants3...>> parameter3,
337 : std::tuple<size_t, tmpl::list<IntegralConstants4...>> parameter4,
338 : const Args&... parameters) const {
339 : constexpr size_t num0 = sizeof...(IntegralConstants0);
340 : constexpr size_t num1 = sizeof...(IntegralConstants1);
341 : constexpr size_t num2 = sizeof...(IntegralConstants2);
342 : constexpr size_t num3 = sizeof...(IntegralConstants3);
343 : constexpr size_t num4 = sizeof...(IntegralConstants4);
344 : constexpr size_t total_size = num0 * num1 * num2 * num3 * num4;
345 : // note that the act of assigning to the specified function pointer type
346 : // fixes the template arguments that need to be inferred.
347 : static const std::array<const T& (StaticCache::*)(Args...) const,
348 : total_size>
349 : cache = []() {
350 : std::array<const T& (StaticCache::*)(Args...) const, total_size>
351 : result;
352 : size_t counter4 = 0;
353 : const auto helper4 = [&counter4,
354 : &result]<typename IntegralConstant4>() {
355 : size_t counter3 = 0;
356 : const auto helper3 = [&counter3, &counter4,
357 : &result]<typename IntegralConstant3>() {
358 : size_t counter2 = 0;
359 : const auto helper2 = [&counter2, &counter3, &counter4,
360 : &result]<typename IntegralConstant2>() {
361 : size_t counter1 = 0;
362 : const auto helper1 = [&counter1, &counter2, &counter3,
363 : &counter4,
364 : &result]<typename IntegralConstant1>() {
365 : size_t counter0 = 0;
366 : const auto helper0 = [&counter0, &counter1, &counter2,
367 : &counter3, &counter4,
368 : &result]<typename IntegralConstant0>() {
369 : result[counter0 +
370 : num0 *
371 : (counter1 +
372 : num1 * (counter2 +
373 : num2 * (counter3 + num3 * counter4)))] =
374 : &StaticCache::unwrap_cache_combined<
375 : IntegralConstantValues..., IntegralConstant0,
376 : IntegralConstant1, IntegralConstant2,
377 : IntegralConstant3, IntegralConstant4>;
378 : ++counter0;
379 : };
380 : EXPAND_PACK_LEFT_TO_RIGHT(
381 : helper0.template operator()<IntegralConstants0>());
382 : ++counter1;
383 : };
384 : EXPAND_PACK_LEFT_TO_RIGHT(
385 : helper1.template operator()<IntegralConstants1>());
386 : ++counter2;
387 : };
388 : EXPAND_PACK_LEFT_TO_RIGHT(
389 : helper2.template operator()<IntegralConstants2>());
390 : ++counter3;
391 : };
392 : EXPAND_PACK_LEFT_TO_RIGHT(
393 : helper3.template operator()<IntegralConstants3>());
394 : ++counter4;
395 : };
396 : EXPAND_PACK_LEFT_TO_RIGHT(
397 : helper4.template operator()<IntegralConstants4>());
398 : return result;
399 : }();
400 :
401 : // The array `cache` holds pointers to member functions, so we dereference
402 : // the pointer and invoke it on `this`.
403 : return (
404 : this->*gsl::at(
405 : cache,
406 : std::get<0>(parameter0) +
407 : num0 *
408 : (std::get<0>(parameter1) +
409 : num1 * (std::get<0>(parameter2) +
410 : num2 * (std::get<0>(parameter3) +
411 : num3 * std::get<0>(parameter4))))))(
412 : parameters...);
413 : }
414 : #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ > 10 && __GNUC__ < 14
415 : #pragma GCC diagnostic pop
416 : #endif
417 :
418 0 : const Generator generator_;
419 : };
420 :
421 : /// \ingroup UtilitiesGroup
422 : /// Create a StaticCache, inferring the cached type from the generator.
423 : template <typename... Ranges, typename Generator>
424 1 : auto make_static_cache(Generator&& generator) {
425 : using CachedType = std::remove_cv_t<decltype(generator(
426 : std::declval<typename Ranges::value_type>()...))>;
427 : return StaticCache<std::remove_cv_t<Generator>, CachedType, Ranges...>(
428 : std::forward<Generator>(generator));
429 : }
|