Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
LRUCache.h
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#ifndef _Stroika_Foundation_Cache_LRUCache_h_
5#define _Stroika_Foundation_Cache_LRUCache_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <optional>
10#include <vector>
11
14#include "Stroika/Foundation/Common/Common.h"
15#include "Stroika/Foundation/Common/Concepts.h"
17#include "Stroika/Foundation/Containers/Mapping.h"
20
21/**
22 * \file
23 *
24 * TODO:
25 * @todo Find some reasonable/simple way to get
26 * LRUCache<PHRShortcutSpec, PHRShortcutSpec, PHRShortcutSpecNoAuthCacheTraits_> sRecentlyUsedCache (kMaxEltsInReceltlyUsedCache_);
27 * Working with ONE T argument
28 * Add(elt2cache).
29 *
30 * PROBABLY add overload of Add() with one argument, IF VALUETYPE == KEYTYPE?
31 *
32 * ADDED EXPERIMENTALLY in v2.1d6
33 *
34 * But - REVIEW that usage to make sure it makes sense. Explain better here if/why it does.
35 *
36 * @todo Currently we have redundant storage - _Buf, and _First, and _Last (really just need _Buf cuz
37 * has first/last, or do our own storage managemnet with secondary array? - we do the mallocs/frees.
38 * To re-free, even though order distorted by shuffles, we can always figure out which was
39 * the original array head by which has the lowest address!
40 *
41 * Also somewhat related, _Last usage is C++ unconvnetional - though maybe OK. If not more awkward
42 * in impl, consider using _fEnd? Or if it is (I think last maybe better then document clearly why
43 * its better.
44 *
45 */
46
48
49 /**
50 * \brief LRUCache implements a simple least-recently-used caching strategy, with optional hashing (of keys) to make it faster.
51 *
52 * \note Comparison with TimedCache
53 * The main difference between an LRUCache and TimedCache has to do with when an element is evicted from the Cache.
54 * With a TimedCache, its evicted only when its overly aged (now - when added to cache). With an LRUCache, its more random, and depends a
55 * bit on luck (when using hashing) and how recently an item was last accessed.
56 *
57 * \par Example Usage
58 * \code
59 * LRUCache<string, string> tmp{3}; // no hashing used in cache
60 * tmp.Add ("a", "1");
61 * tmp.Add ("b", "2");
62 * tmp.Add ("c", "3");
63 * tmp.Add ("d", "4");
64 * EXPECT_TRUE (not tmp.Lookup ("a").has_value ());
65 * EXPECT_TRUE (tmp.Lookup ("b") == "2");
66 * EXPECT_TRUE (tmp.Lookup ("d") == "4");
67 * \endcode
68 *
69 * \par Example Usage
70 * \code
71 * // using deduction guides, and hash table of size 10
72 * LRUCache tmp{pair<string, string>{}, 3, 10, hash<string>{}};
73 * tmp.Add ("a", "1");
74 * tmp.Add ("b", "2");
75 * tmp.Add ("c", "3");
76 * tmp.Add ("d", "4");
77 * EXPECT_TRUE (not tmp.Lookup ("a").has_value () or *tmp.Lookup ("a") == "1"); // could be missing or found but if found same value
78 * EXPECT_TRUE (tmp.Lookup ("b") == "2");
79 * EXPECT_TRUE (tmp.Lookup ("d") == "4");
80 * \endcode
81 *
82 * \note LRUCache destroys objects when they are cleared from the cache. This guarantee is
83 * relevant only in case where the objects use significant resources, or where their lifetime has
84 * externally visible (e.g. lockfiles) impact.
85 *
86 * \note <a href="Design-Overview.md#Comparisons">Comparisons</a>:
87 * o No comparison of LRUCache objects is currently supported. It might make sense, but would be of questionable use.
88 *
89 * \note \em Thread-Safety <a href="Thread-Safety.md#C++-Standard-Thread-Safety">C++-Standard-Thread-Safety</a>
90 *
91 * @see SynchronizedLRUCache<> - internally synchronized version
92 */
93 template <typename KEY, typename VALUE, typename KEY_EQUALS_COMPARER = equal_to<KEY>, typename KEY_HASH_FUNCTION = nullptr_t, typename STATS_TYPE = Statistics::StatsType_DEFAULT>
94 class LRUCache {
95 public:
96 using KeyType = KEY;
97
98 public:
99 using KeyEqualsCompareFunctionType = KEY_EQUALS_COMPARER;
100
101 public:
102 using StatsType = STATS_TYPE;
103
104 public:
105 /**
106 * There are two basic kinds of LRUCache - with hashing, and without.
107 *
108 * If there is no KEY_HASH_FUNCTION (==nullptr) - then the GetHashTableSize () always returns 1;
109 *
110 * Note the hash function can be hash<KEY_TYPE>{}, and this is one of the constructor defaults.
111 *
112 * Note cannot move easily because this contains internal pointers (fCachedElts_First_): still declare move CTOR, but its not
113 * noexcept because its really copying...
114 *
115 * Because of a couple key limitations/constraints in C++ (as of C++20) - you cannot both do template argument deduction, and default parameters).
116 * This greatly constrains how the class works (at least constructors).
117 *
118 * So this is somewhat subject to change as the language evolves (or my understanding of tricks evolves). But for now, deduction is limited.
119 *
120 * \par Example Usage
121 * \code
122 * LRUCache<string, string> tmp{3}; // no hashing, size 3, no deduced types (just defaulted ones)
123 * LRUCache t0{Factory::LRUCache_WithHash<string, string>{}(3, 3)};
124 * LRUCache t1{Factory::LRUCache_WithHash<String, string>{}(3, 3, hashFunction)}; // types (except key/value) deducted from arguments
125 * \endcode
126 *
127 * \todo default CTOR requires no hashing, but we could make hashing work in this case with default params - just not worth it yet --LGP 2023-12-06
128 */
129 LRUCache ()
130 requires (same_as<KEY_HASH_FUNCTION, nullptr_t> and same_as<KEY_EQUALS_COMPARER, equal_to<KEY>>);
131 LRUCache (size_t maxCacheSize, const KEY_EQUALS_COMPARER& keyEqualsComparer = {})
132 requires (same_as<KEY_HASH_FUNCTION, nullptr_t>);
133 LRUCache (size_t maxCacheSize, const KEY_EQUALS_COMPARER& keyEqualsComparer = {}, size_t hashTableSize = 1,
134 const KEY_HASH_FUNCTION& hashFunction = KEY_HASH_FUNCTION{})
135 requires (not same_as<KEY_HASH_FUNCTION, nullptr_t>);
136 LRUCache (size_t maxCacheSize, size_t hashTableSize, const KEY_HASH_FUNCTION& hashFunction = KEY_HASH_FUNCTION{})
137 requires (not same_as<KEY_HASH_FUNCTION, nullptr_t>);
138
139 LRUCache (LRUCache&& from);
140 LRUCache (const LRUCache& from)
141 requires (same_as<KEY_HASH_FUNCTION, nullptr_t>);
142 LRUCache (const LRUCache& from)
143 requires (not same_as<KEY_HASH_FUNCTION, nullptr_t>);
144
145 public:
146 /**
147 */
148 nonvirtual LRUCache& operator= (LRUCache&& rhs) noexcept;
149 nonvirtual LRUCache& operator= (const LRUCache& rhs);
150
151 public:
152 /**
153 */
154 nonvirtual size_t GetMaxCacheSize () const;
155
156 public:
157 /**
158 * Size given maybe automatically adjusted upward to be a multiple of GetHashTableSize ()
159 */
160 nonvirtual void SetMaxCacheSize (size_t maxCacheSize);
161
162 public:
163 /**
164 */
165 nonvirtual KeyEqualsCompareFunctionType GetKeyEqualsCompareFunction () const;
166
167 public:
168 /**
169 */
170 nonvirtual StatsType GetStats () const;
171
172 public:
173 /**
174 */
175 nonvirtual size_t GetHashTableSize () const;
176
177 public:
178 /**
179 */
180 nonvirtual KEY_HASH_FUNCTION GetKeyHashFunction () const;
181
182 public:
183 /**
184 * Clear all, or just the given elements from the cache.
185 */
186 nonvirtual void clear ();
187 nonvirtual void clear (typename Common::ArgByValueType<KEY> key);
188 nonvirtual void clear (function<bool (typename Common::ArgByValueType<KEY>)> clearPredicate);
189
190 public:
191 /**
192 * The value associated with KEY may not be present, so an missing optional value is returned.
193 *
194 * \note Unintuitively, Lookup () is non-const **intentionally** - because it updates internal data structures to track the most recently accessed item. This has implication for thread-safety!
195 *
196 * @see LookupValue ()
197 */
199
200 public:
201 /**
202 * LookupValue () finds the value in the cache, and returns it, or if not present, uses the argument valueFetcher to retrieve it.
203 *
204 * So LookupValue (v) is equivalent to:
205 * \code
206 * if (auto o = Lookup (k)) {
207 * return o;
208 * }
209 * else {
210 * auto v = valueFetcher (k);
211 * Add (k, v);
212 * return v;
213 * }
214 * \endcode
215 *
216 * \par Example Usage
217 * \code
218 * struct Details_ {
219 * };
220 * using DetailsID = int;
221 * Details_ ReadDetailsFromFile_ (DetailsID id);
222 *
223 * Execution::Synchronized<LRUCache<DetailsID, Details_>> fDetailsCache_; // caches often helpful in multithreaded situations
224 *
225 * // returns the value from LRUCache, or automatically pages it in from file
226 * Details_ GetDetails (DetailsID id) {
227 * return
228 * fDetailsCache_->LookupValue (
229 * id,
230 * [] (DetailsID id) -> Details_ { return ReadDetailsFromFile_ (id); }
231 * );
232 * }
233 * \endcode
234 *
235 * \note - LookupValue () only caches successful lookups, and propagates any exceptions looking up.
236 * To negatively cache, be sure you use an optional<X> for the VALUE type, and then you can wrap
237 * the LookupValue function with try/catch and on failure, cache nullopt.
238 */
239 nonvirtual VALUE LookupValue (typename Common::ArgByValueType<KEY> key, const function<VALUE (typename Common::ArgByValueType<KEY>)>& valueFetcher);
240
241 public:
242 /**
243 * Add the given value to the cache. This is rarely directly used.
244 * Typically you Lookup with something like LookupValue() which implicitly does the adds.
245 */
246 nonvirtual void Add (typename Common::ArgByValueType<KEY> key, typename Common::ArgByValueType<VALUE> value);
247 nonvirtual void Add (typename Common::ArgByValueType<KEY> key)
248 requires (same_as<KEY, VALUE>);
249
250 public:
251 /**
252 * Collect all the elements of the cache, where mapping KEY and VALUE correspond to cache KEY and VALUE.
253 */
254 nonvirtual Containers::Mapping<KEY, VALUE> Elements () const;
255
256 public:
257 // * \note the overloads taking pair<KEY, VALUE> as the first argument are just tricks to allow deduction guides to
258 // work (because* you cannot specify some template parameters and then have deduction guides take effect)
259 // .
260 // find better way todo deduction guides so I can deprecate this
261 [[deprecated ("Since Stroika v3.0d5 use Cache::Factory::LRUCache_WithHash or NoHash")]] LRUCache (
262 pair<KEY, VALUE> ignored, size_t maxCacheSize = 1, const KEY_EQUALS_COMPARER& keyEqualsComparer = {}, size_t hashTableSize = 1,
265 {
266 }
267 [[deprecated ("Since Stroika v3.0d5 use Cache::Factory::LRUCache_WithHash or NoHash")]] LRUCache (
270 {
271 }
272
273 private:
274 const size_t fHashtableSize_{1};
275
276 private:
277 struct KeyValuePair_ {
278 KEY fKey;
279 VALUE fValue;
280 };
281
282 private:
283 // invoke selected hash function, and return number 0..fHashtableSize_
284 nonvirtual size_t H_ (typename Common::ArgByValueType<KEY> k) const;
285
286 private:
287 [[no_unique_address]] Debug::AssertExternallySynchronizedMutex fAssertExternallySyncrhonized_;
288
289 private:
290 [[no_unique_address]] const KeyEqualsCompareFunctionType fKeyEqualsComparer_;
291 [[no_unique_address]] const KEY_HASH_FUNCTION fHashFunction_;
292 [[no_unique_address]] STATS_TYPE fStats_;
293
294 struct CacheElement_;
295 struct CacheIterator_;
296
297 nonvirtual CacheIterator_ begin_ () const;
298 nonvirtual CacheIterator_ end_ () const;
299
300 nonvirtual void ClearCache_ ();
301
302 /*
303 * Create a new LRUCache_ element (potentially bumping some old element out of the cache). This new element
304 * will be considered most recently used. Note that this routine re-orders the cache so that the most recently looked
305 * up element is first, and because of this re-ordering, its illegal to do a Lookup while
306 * a @'LRUCache_<ELEMENT>::CacheIterator_' exists for this LRUCache_.</p>
307 */
308 nonvirtual optional<KeyValuePair_>* AddNew_ (typename Common::ArgByValueType<KeyType> item);
309
310 /*
311 * Check and see if the given element is in the cache. Return that element if its there, and nullptr otherwise.
312 * Note that this routine re-orders the cache so that the most recently looked up element is first, and because
313 * of this re-ordering, its illegal to do a Lookup while a @'LRUCache_<ELEMENT>::CacheIterator_' exists
314 * for this LRUCache_.
315 */
316 nonvirtual optional<KeyValuePair_>* LookupElement_ (typename Common::ArgByValueType<KeyType> item);
317
318 /*
319 */
320 nonvirtual void ShuffleToHead_ (size_t chainIdx, CacheElement_* b);
321
322 static constexpr size_t kPreallocatedHashtableSize_ = same_as<KEY_HASH_FUNCTION, nullptr_t> ? 1 : 5; // size where no memory allocation overhead for lrucache
323 Memory::InlineBuffer<vector<CacheElement_>, kPreallocatedHashtableSize_> fCachedElts_BUF_{};
326 };
327
328 namespace Factory {
329 /**
330 * \note - no way to extract the KEY from the KEY_EQUALS_COMPARER, because this comparer might have templated operator(), such
331 * as String::EqualsComparer.
332 *
333 * \par Example Usage
334 * \code
335 * auto t0{Factory::LRUCache_NoHash<string, string>{}()};
336 * auto t1{Factory::LRUCache_NoHash<string, string>{}(3)};
337 * LRUCache t2{Factory::LRUCache_NoHash<String, string>{}(3, kStringCIComparer_)};
338 * \endcode
339 */
340 template <typename KEY, typename VALUE, typename STATS_TYPE = Statistics::StatsType_DEFAULT>
342 template <Common::IEqualsComparer<KEY> KEY_EQUALS_COMPARER = equal_to<KEY>>
343 auto operator() (size_t maxCacheSize = 1, const KEY_EQUALS_COMPARER& keyComparer = {}) const
344 {
346 }
347 };
348
349 /**
350 * \par Example Usage
351 * \code
352 * auto t0{Factory::LRUCache_WithHash<string, string>{}(3, 3)};
353 * auto t1{Factory::LRUCache_WithHash<String, string>{}(3, 3, hashFunction)};
354 * LRUCache t2{Factory::LRUCache_WithHash<String, string>{}(3, equal_to<String>{}, 3)};
355 * LRUCache t3{Factory::LRUCache_WithHash<String, string, Statistics::Stats_Basic>{}(3, equal_to<String>{}, 3)}; // throw in stats object
356 * LRUCache t4{Factory::LRUCache_WithHash<String, string>{}(3, kStringCIComparer_, 3)}; // alt equality comparer
357 * \endcode
358 */
359 template <typename KEY, typename VALUE, typename STATS_TYPE = Statistics::StatsType_DEFAULT, typename DEFAULT_KEY_EQUALS_COMPARER = equal_to<KEY>>
361 template <typename KEY_HASH_FUNCTION = hash<KEY>>
362 auto operator() (size_t maxCacheSize, size_t hastTableSize, const KEY_HASH_FUNCTION& hashFunction = {}) const
363 {
364 Require (maxCacheSize >= hastTableSize);
367 }
368 template <typename KEY_EQUALS_COMPARER, typename KEY_HASH_FUNCTION = hash<KEY>>
369 auto operator() (size_t maxCacheSize, const KEY_EQUALS_COMPARER& keyComparer, size_t hastTableSize,
370 const KEY_HASH_FUNCTION& hashFunction = {}) const
371 {
372 Require (maxCacheSize >= hastTableSize);
374 }
375 };
376
377 }
378
379}
380
381/*
382 ********************************************************************************
383 ***************************** Implementation Details ***************************
384 ********************************************************************************
385 */
386#include "LRUCache.inl"
387
388#endif /*_Stroika_Foundation_Cache_LRUCache_h_*/
LRUCache implements a simple least-recently-used caching strategy, with optional hashing (of keys) to...
Definition LRUCache.h:94
nonvirtual Containers::Mapping< KEY, VALUE > Elements() const
Definition LRUCache.inl:290
nonvirtual void Add(typename Common::ArgByValueType< KEY > key, typename Common::ArgByValueType< VALUE > value)
Definition LRUCache.inl:265
nonvirtual void SetMaxCacheSize(size_t maxCacheSize)
Definition LRUCache.inl:176
nonvirtual VALUE LookupValue(typename Common::ArgByValueType< KEY > key, const function< VALUE(typename Common::ArgByValueType< KEY >)> &valueFetcher)
Definition LRUCache.inl:302
nonvirtual optional< VALUE > Lookup(typename Common::ArgByValueType< KEY > key)
Definition LRUCache.inl:253
NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured thr...
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
conditional_t<(sizeof(CHECK_T)<=2 *sizeof(void *)) and is_trivially_copyable_v< CHECK_T >, CHECK_T, const CHECK_T & > ArgByValueType
This is an alias for 'T' - but how we want to pass it on stack as formal parameter.
Definition TypeHints.h:32