From 7039002a32fb6b59221139b8d72b538339b75e98 Mon Sep 17 00:00:00 2001 From: Ben Deane Date: Thu, 15 May 2025 16:33:11 -0600 Subject: [PATCH] :sparkles: Add `latched` Problem: - Some cached values have `set-once` semantics. It would be a bad idea to reset them to something else. Solution: - Add `latched` to make this clear. --- CMakeLists.txt | 3 ++ docs/index.adoc | 1 + docs/latched.adoc | 45 +++++++++++++++++++++ include/stdx/cached.hpp | 84 +++++---------------------------------- include/stdx/latched.hpp | 61 ++++++++++++++++++++++++++++ include/stdx/rollover.hpp | 2 + test/CMakeLists.txt | 1 + test/cached.cpp | 4 +- test/latched.cpp | 80 +++++++++++++++++++++++++++++++++++++ 9 files changed, 206 insertions(+), 75 deletions(-) create mode 100644 docs/latched.adoc create mode 100644 include/stdx/latched.hpp create mode 100644 test/latched.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a784133..e28ffa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,13 +67,16 @@ target_sources( include/stdx/intrusive_forward_list.hpp include/stdx/intrusive_list.hpp include/stdx/iterator.hpp + include/stdx/latched.hpp include/stdx/memory.hpp include/stdx/numeric.hpp include/stdx/optional.hpp include/stdx/panic.hpp include/stdx/priority.hpp include/stdx/ranges.hpp + include/stdx/rollover.hpp include/stdx/span.hpp + include/stdx/static_assert.hpp include/stdx/tuple_algorithms.hpp include/stdx/tuple_destructure.hpp include/stdx/tuple.hpp diff --git a/docs/index.adoc b/docs/index.adoc index 6b2c496..321568a 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -31,6 +31,7 @@ include::functional.adoc[] include::intrusive_forward_list.adoc[] include::intrusive_list.adoc[] include::iterator.adoc[] +include::latched.adoc[] include::memory.adoc[] include::numeric.adoc[] include::optional.adoc[] diff --git a/docs/latched.adoc b/docs/latched.adoc new file mode 100644 index 0000000..f40e6a4 --- /dev/null +++ b/docs/latched.adoc @@ -0,0 +1,45 @@ + +== `latched.hpp` + +A `latched` value represents a value that is computed on demand once, and +latched (`const`) thereafter. It is constructed with a lambda expression that +will compute the value when needed. + +A `latched` value is similar to a xref:cached.adoc#_cached_hpp[`cached`] value except that +once computed, a `latched` value cannot be reset. + +[source,cpp] +---- +constexpr auto c = stdx::latched{[] { return expensive_computation(); }}; +---- + +A `latched` value is something like a `std::optional` and supports some similar +functionality. Note though that any kind of "dereference" operation +automatically computes the value if needed. + +[source,cpp] +---- +// check whether the value is present +auto b = c.has_value(); + +// or, automatic bool conversion (explicit) +if (c) { + // do something +} + +// use the value (computing where necessary) +auto value = *c; +auto alt_value = c.value(); +auto value_member = c->member; +---- + +If needed, the type of the latched value can obtained with `latched_value_t`. + +[source,cpp] +---- +auto c = stdx::latched{[] { return expensive_computation(); }}; +using V = stdx::latched_value_t; +---- + +NOTE: You can also use `typename decltype(c)::value_type`, but if the type of `c` +has cvref qualifiers, `latched_value_t` saves the bother of using `remove_cvref_t`. diff --git a/include/stdx/cached.hpp b/include/stdx/cached.hpp index 8968f6d..8b12115 100644 --- a/include/stdx/cached.hpp +++ b/include/stdx/cached.hpp @@ -1,86 +1,24 @@ #pragma once #include -#include +#include #include -#include -#include -#include - namespace stdx { inline namespace v1 { -template struct cached { - using value_type = stdx::remove_cvref_t>; - - constexpr explicit cached(F const &f) : lazy{f} {} - constexpr explicit cached(F &&f) : lazy{std::move(f)} {} - - constexpr auto has_value() const noexcept -> bool { - return opt.has_value(); - } - constexpr explicit operator bool() const noexcept { - return opt.has_value(); - } - - constexpr auto value() & LIFETIMEBOUND -> value_type & { - populate(); - return *opt; - } - constexpr auto value() const & LIFETIMEBOUND -> value_type const & { - populate(); - return *opt; - } - constexpr auto value() && LIFETIMEBOUND -> value_type && { - populate(); - return *std::move(opt); - } - constexpr auto value() const && LIFETIMEBOUND -> value_type const && { - populate(); - return *std::move(opt); - } - - constexpr auto operator->() const LIFETIMEBOUND->value_type const * { - populate(); - return opt.operator->(); - } - constexpr auto operator->() LIFETIMEBOUND->value_type * { - populate(); - return opt.operator->(); - } +template struct cached : latched { + using latched::latched; - constexpr auto operator*() const & LIFETIMEBOUND->decltype(auto) { - return value(); - } - constexpr auto operator*() & LIFETIMEBOUND->decltype(auto) { - return value(); + auto reset() { this->opt.reset(); } + auto refresh() LIFETIMEBOUND -> typename latched::value_type & { + this->opt.reset(); + this->populate(); + return *this->opt; } - constexpr auto operator*() const && LIFETIMEBOUND->decltype(auto) { - return std::move(*this).value(); - } - constexpr auto operator*() && LIFETIMEBOUND->decltype(auto) { - return std::move(*this).value(); - } - - auto reset() { opt.reset(); } - auto refresh() LIFETIMEBOUND -> value_type & { - opt.reset(); - populate(); - return *opt; - } - - private: - constexpr auto populate() const { - if (not opt.has_value()) { - opt.emplace(lazy); - } - } - - with_result_of lazy; - mutable std::optional opt{}; }; -template -using cached_value_t = typename stdx::remove_cvref_t::value_type; +template cached(F) -> cached>; + +template using cached_value_t = latched_value_t; } // namespace v1 } // namespace stdx diff --git a/include/stdx/latched.hpp b/include/stdx/latched.hpp new file mode 100644 index 0000000..20a78c7 --- /dev/null +++ b/include/stdx/latched.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace stdx { +inline namespace v1 { +template struct latched { + using value_type = stdx::remove_cvref_t>; + + constexpr explicit latched(F const &f) : lazy{f} {} + constexpr explicit latched(F &&f) : lazy{std::move(f)} {} + + constexpr auto has_value() const noexcept -> bool { + return opt.has_value(); + } + constexpr explicit operator bool() const noexcept { + return opt.has_value(); + } + + constexpr auto value() const & LIFETIMEBOUND -> value_type const & { + populate(); + return *opt; + } + constexpr auto value() const && LIFETIMEBOUND -> value_type const && { + populate(); + return *std::move(opt); + } + + constexpr auto operator->() const LIFETIMEBOUND->value_type const * { + populate(); + return opt.operator->(); + } + + constexpr auto operator*() const & LIFETIMEBOUND->decltype(auto) { + return value(); + } + constexpr auto operator*() const && LIFETIMEBOUND->decltype(auto) { + return std::move(*this).value(); + } + + protected: + constexpr auto populate() const { + if (not opt.has_value()) { + opt.emplace(lazy); + } + } + + with_result_of lazy; + mutable std::optional opt{}; +}; + +template +using latched_value_t = typename stdx::remove_cvref_t::value_type; +} // namespace v1 +} // namespace stdx diff --git a/include/stdx/rollover.hpp b/include/stdx/rollover.hpp index 69f6d29..1f52c93 100644 --- a/include/stdx/rollover.hpp +++ b/include/stdx/rollover.hpp @@ -12,6 +12,7 @@ template struct rollover_t { using underlying_t = T; constexpr rollover_t() = default; + // NOLINTBEGIN(modernize-use-constraints) template >> constexpr explicit rollover_t(U u) : value{static_cast(u)} {} @@ -19,6 +20,7 @@ template struct rollover_t { typename = std::enable_if_t>> constexpr explicit rollover_t(rollover_t u) : rollover_t{static_cast(u)} {} + // NOLINTEND(modernize-use-constraints) [[nodiscard]] constexpr auto as_underlying() const -> underlying_t { return value; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1f73f6b..e4fd72c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -48,6 +48,7 @@ add_tests( intrusive_list_properties is_constant_evaluated iterator + latched memory numeric optional diff --git a/test/cached.cpp b/test/cached.cpp index 8b717d9..c7a86c7 100644 --- a/test/cached.cpp +++ b/test/cached.cpp @@ -94,8 +94,8 @@ TEST_CASE("non-movable value", "[cached]") { TEST_CASE("preserving value categories", "[cached]") { { auto c = stdx::cached{[] { return 42; }}; - static_assert(std::is_same_v); - static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); } { auto const c = stdx::cached{[] { return 42; }}; diff --git a/test/latched.cpp b/test/latched.cpp new file mode 100644 index 0000000..77cc4f6 --- /dev/null +++ b/test/latched.cpp @@ -0,0 +1,80 @@ +#include + +#include + +TEST_CASE("construction", "[latched]") { + auto c = stdx::latched{[] { return 42; }}; + CHECK(not c); + CHECK(not c.has_value()); +} + +TEST_CASE("exposed value_type", "[latched]") { + auto c = stdx::latched{[] { return 42; }}; + static_assert(std::is_same_v); +} + +TEST_CASE("latched_value_t", "[latched]") { + auto const c = stdx::latched{[] { return 42; }}; + static_assert(std::is_same_v, int>); +} + +TEST_CASE("automatic population", "[latched]") { + auto c = stdx::latched{[] { return 42; }}; + CHECK(c.value() == 42); + CHECK(c.has_value()); +} + +TEST_CASE("operator*", "[latched]") { + auto c = stdx::latched{[] { return 42; }}; + CHECK(*c == 42); + CHECK(c.has_value()); +} + +namespace { +struct S { + int x{}; +}; +} // namespace + +TEST_CASE("operator->", "[latched]") { + auto c = stdx::latched{[] { return S{42}; }}; + CHECK(c->x == 42); + CHECK(c.has_value()); +} + +namespace { +struct move_only { + constexpr move_only(int i) : value{i} {} + constexpr move_only(move_only &&) = default; + int value{}; +}; + +struct non_movable { + constexpr non_movable(int i) : value{i} {} + constexpr non_movable(non_movable &&) = delete; + int value{}; +}; +} // namespace + +TEST_CASE("move-only value", "[latched]") { + auto c = stdx::latched{[] { return move_only{42}; }}; + CHECK(c->value == 42); +} + +TEST_CASE("non-movable value", "[latched]") { + auto c = stdx::latched{[] { return non_movable{42}; }}; + CHECK(c->value == 42); +} + +TEST_CASE("preserving value categories", "[latched]") { + { + auto c = stdx::latched{[] { return 42; }}; + static_assert(std::is_same_v); + static_assert(std::is_same_v); + } + { + auto const c = stdx::latched{[] { return 42; }}; + static_assert(std::is_same_v); + static_assert(std::is_same_v); + } +}