diff --git a/src/tech/include/cachedresult.hpp b/src/tech/include/cachedresult.hpp index 92d664b1..ba2cbfe6 100644 --- a/src/tech/include/cachedresult.hpp +++ b/src/tech/include/cachedresult.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -7,10 +9,14 @@ #include #include "cachedresultvault.hpp" +#include "cct_fixedcapacityvector.hpp" #include "cct_hash.hpp" +#include "cct_type_traits.hpp" #include "timedef.hpp" namespace cct { + +namespace details { template class CachedResultOptionsT { public: @@ -21,18 +27,22 @@ class CachedResultOptionsT { private: template - friend class CachedResultT; + friend class CachedResultWithArgs; + + template + friend class CachedResultWithoutArgs; DurationT _refreshPeriod; CachedResultVaultT *_pCacheResultVault = nullptr; }; +} // namespace details + +using CachedResultOptions = details::CachedResultOptionsT; -using CachedResultOptions = CachedResultOptionsT; +namespace details { -/// Wrapper of an object of type T (should be a functor) for which the underlying method is called at most once per -/// given period of time. May be useful to automatically cache some API calls in an easy and efficient way. template -class CachedResultT : public CachedResultBase { +class CachedResultWithArgs : public CachedResultBase { public: using ResultType = std::remove_cvref_t()(std::declval()...))>; using TimePoint = ClockT::time_point; @@ -56,13 +66,20 @@ class CachedResultT : public CachedResultBase { public: template - explicit CachedResultT(CachedResultOptionsT opts, TArgs &&...args) + explicit CachedResultWithArgs(CachedResultOptionsT opts, TArgs &&...args) : CachedResultBase(opts._refreshPeriod), _func(std::forward(args)...) { if (opts._pCacheResultVault) { opts._pCacheResultVault->registerCachedResult(*this); } } + CachedResultWithArgs(const CachedResultWithArgs &) = delete; + CachedResultWithArgs(CachedResultWithArgs &&) = delete; + CachedResultWithArgs &operator=(const CachedResultWithArgs &) = delete; + CachedResultWithArgs &operator=(CachedResultWithArgs &&) = delete; + + ~CachedResultWithArgs() = default; + /// Sets given value associated to the key built with given parameters, /// if given timestamp is more recent than the one associated to the value already present at this key (if any) /// refresh period is not checked, if given timestamp is more recent than the one associated to given value, cache @@ -71,8 +88,8 @@ class CachedResultT : public CachedResultBase { void set(ResultTypeT &&val, TimePoint timePoint, Args &&...funcArgs) { checkPeriodicRehash(); - auto [it, isInserted] = _cachedResultsMap.try_emplace(TKey(std::forward(funcArgs)...), - std::forward(val), timePoint); + auto [it, isInserted] = + _data.try_emplace(TKey(std::forward(funcArgs)...), std::forward(val), timePoint); if (!isInserted && it->second.lastUpdatedTs < timePoint) { it->second = Value(std::forward(val), timePoint); } @@ -85,16 +102,16 @@ class CachedResultT : public CachedResultBase { const auto nowTime = ClockT::now(); if (this->_state == State::kForceUniqueRefresh) { - _cachedResultsMap.clear(); + _data.clear(); + this->_state = State::kForceCache; } else { checkPeriodicRehash(); } const auto flattenTuple = [this](auto &&...values) { return _func(std::forward(values)...); }; - TKey key(std::forward(funcArgs)...); - auto [it, isInserted] = _cachedResultsMap.try_emplace(key, flattenTuple, key, nowTime); + auto [it, isInserted] = _data.try_emplace(key, flattenTuple, key, nowTime); if (!isInserted && this->_state != State::kForceCache && this->_refreshPeriod < nowTime - it->second.lastUpdatedTs) { it->second = Value(flattenTuple, std::move(key), nowTime); @@ -106,8 +123,8 @@ class CachedResultT : public CachedResultBase { /// If no value has been computed for this key, returns a nullptr. template std::pair retrieve(Args &&...funcArgs) const { - auto it = _cachedResultsMap.find(TKey(std::forward(funcArgs)...)); - if (it == _cachedResultsMap.end()) { + auto it = _data.find(TKey(std::forward(funcArgs)...)); + if (it == _data.end()) { return {}; } return {std::addressof(it->second.result), it->second.lastUpdatedTs}; @@ -123,25 +140,117 @@ class CachedResultT : public CachedResultBase { const auto nowTime = ClockT::now(); - for (auto it = _cachedResultsMap.begin(); it != _cachedResultsMap.end();) { + for (auto it = _data.begin(); it != _data.end();) { if (this->_refreshPeriod < nowTime - it->second.lastUpdatedTs) { // Data has expired, remove it - it = _cachedResultsMap.erase(it); + it = _data.erase(it); } else { ++it; } } - _cachedResultsMap.rehash(_cachedResultsMap.size()); + _data.rehash(_data.size()); + } + + T _func; + std::unordered_map _data; +}; + +/// Optimization when there is no key. +/// Data is stored inlined in the CachedResult object in this case. +template +class CachedResultWithoutArgs : public CachedResultBase { + public: + using ResultType = std::remove_cvref_t()())>; + using TimePoint = ClockT::time_point; + using Duration = ClockT::duration; + using State = CachedResultBase::State; + + template + explicit CachedResultWithoutArgs(CachedResultOptionsT opts, TArgs &&...args) + : CachedResultBase(opts._refreshPeriod), _func(std::forward(args)...) { + if (opts._pCacheResultVault) { + opts._pCacheResultVault->registerCachedResult(*this); + } + } + + CachedResultWithoutArgs(const CachedResultWithoutArgs &) = delete; + CachedResultWithoutArgs(CachedResultWithoutArgs &&) = delete; + CachedResultWithoutArgs &operator=(const CachedResultWithoutArgs &) = delete; + CachedResultWithoutArgs &operator=(CachedResultWithoutArgs &&) = delete; + + ~CachedResultWithoutArgs() = default; + + /// Sets given value for given time stamp, if time stamp currently associated to last value is older. + template + void set(ResultTypeT &&val, TimePoint timePoint) { + if (_lastUpdatedTs < timePoint) { + if (isResultConstructed()) { + _resultStorage.front() = std::forward(val); + } else { + _resultStorage.push_back(std::forward(val)); + } + + _lastUpdatedTs = timePoint; + } + } + + /// Get the latest value. + /// If the value is too old according to refresh period, it will be recomputed automatically. + const ResultType &get() { + const auto nowTime = ClockT::now(); + + if (this->_state == State::kForceUniqueRefresh) { + _lastUpdatedTs = TimePoint{}; + this->_state = State::kForceCache; + } + + if (_resultStorage.empty() || (this->_refreshPeriod < nowTime - _lastUpdatedTs && + (this->_state != State::kForceCache || _lastUpdatedTs == TimePoint{}))) { + const auto flattenTuple = [this](auto &&...values) { + return _func(std::forward(values)...); + }; + + static constexpr auto kEmptyTuple = std::make_tuple(); + _resultStorage.assign(static_cast(1), std::apply(flattenTuple, kEmptyTuple)); + _lastUpdatedTs = nowTime; + } + + return _resultStorage.front(); } - using MapType = std::unordered_map; + /// Retrieve a {pointer, lastUpdateTime} to latest value stored in this cache. + /// If no value has been computed, returns a nullptr. + std::pair retrieve() const { + return {isResultConstructed() ? _resultStorage.data() : nullptr, _lastUpdatedTs}; + } + + private: + using ResultStorage = FixedCapacityVector; + + [[nodiscard]] bool isResultConstructed() const noexcept { return !_resultStorage.empty(); } T _func; - MapType _cachedResultsMap; + ResultStorage _resultStorage; + TimePoint _lastUpdatedTs; }; -template -using CachedResult = CachedResultT; +template +using CachedResultImpl = std::conditional_t, + CachedResultWithArgs>; +} // namespace details + +/// Wrapper of a functor F for which the underlying method is called at most once per +/// given period of time, provided at construction time. +/// May be useful to automatically cache some API calls in an easy and efficient way. +/// The underlying implementation differs according to FuncTArgs: +/// - if number of FuncTArgs is zero: data is stored inline. Returned pointers / references from get() / retrieve() +/// methods are never invalidated. +/// - otherwise, data is stored in an unordered_map. Returned pointers / references from get() / retrieve() are +/// invalidated by get() calls. +/// In all cases, CachedResult is not moveable nor copyable, because it would require complex logic for +/// CachedResultVault registers based on addresses of objects. +template +using CachedResult = details::CachedResultImpl; -} // namespace cct \ No newline at end of file +} // namespace cct diff --git a/src/tech/include/cachedresultvault.hpp b/src/tech/include/cachedresultvault.hpp index 1001c96c..3d7edd25 100644 --- a/src/tech/include/cachedresultvault.hpp +++ b/src/tech/include/cachedresultvault.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -38,18 +39,14 @@ class CachedResultVaultT { void freezeAll() { if (!_allFrozen) { - for (CachedResultBase *p : _cachedResults) { - p->freeze(); - } + std::ranges::for_each(_cachedResults, [](CachedResultBase *p) { p->freeze(); }); _allFrozen = true; } } void unfreezeAll() noexcept { if (_allFrozen) { - for (CachedResultBase *p : _cachedResults) { - p->unfreeze(); - } + std::ranges::for_each(_cachedResults, [](CachedResultBase *p) { p->unfreeze(); }); _allFrozen = false; } } diff --git a/src/tech/test/cachedresult_test.cpp b/src/tech/test/cachedresult_test.cpp index c8be800c..dc38f7a8 100644 --- a/src/tech/test/cachedresult_test.cpp +++ b/src/tech/test/cachedresult_test.cpp @@ -37,9 +37,9 @@ constexpr SteadyClock::duration kCacheTime = milliseconds(10); constexpr auto kCacheExpireTime = kCacheTime + milliseconds(2); template -using CachedResultSteadyClock = CachedResultT; +using CachedResultSteadyClock = details::CachedResultImpl; -using CachedResultOptionsSteadyClock = CachedResultOptionsT; +using CachedResultOptionsSteadyClock = details::CachedResultOptionsT; using CachedResultVaultSteadyClock = CachedResultVaultT;