diff --git a/score/concurrency/locked_ptr.h b/score/concurrency/locked_ptr.h index 44e6c55ac6..eb9b1775d8 100644 --- a/score/concurrency/locked_ptr.h +++ b/score/concurrency/locked_ptr.h @@ -16,6 +16,7 @@ #include "score/concurrency/type_traits.h" #include "score/concurrency/unlock_guard.h" +#include #include namespace score @@ -192,6 +193,90 @@ class LockedPtr return UnlockGuard{lock_}; } + /** + * @brief Monadic bind. Applies a callable that returns a score::cpp::optional. + * If the pointer is null, short-circuits to score::cpp::nullopt without invoking the callable. + * The callable receives a non-const lvalue reference to this LockedPtr. + * @tparam Func Callable type that accepts LockedPtr& and returns a score::cpp::optional specialization. + * @param f The callable to apply. + * @return The result of f(*this) if non-null, score::cpp::nullopt otherwise. + */ + template >, + typename Ret = std::invoke_result_t, + typename = std::enable_if_t>> + [[nodiscard]] auto and_then(Func&& f) & -> Ret + { + if (ptr_ == nullptr) + { + return score::cpp::nullopt; + } + return std::invoke(std::forward(f), *this); + } + + /** + * @brief Monadic bind. Applies a callable that returns a score::cpp::optional. + * If the pointer is null, short-circuits to score::cpp::nullopt without invoking the callable. + * The callable receives a const lvalue reference to this LockedPtr. + * @tparam Func Callable type that accepts const LockedPtr& and returns a score::cpp::optional specialization. + * @param f The callable to apply. + * @return The result of f(*this) if non-null, score::cpp::nullopt otherwise. + */ + template >, + typename Ret = std::invoke_result_t, + typename = std::enable_if_t>> + [[nodiscard]] auto and_then(Func&& f) const& -> Ret + { + if (ptr_ == nullptr) + { + return score::cpp::nullopt; + } + return std::invoke(std::forward(f), *this); + } + + /** + * @brief Monadic bind. Applies a callable that returns a score::cpp::optional. + * If the pointer is null, short-circuits to score::cpp::nullopt without invoking the callable. + * The callable receives this LockedPtr as an rvalue and may transfer lock ownership by moving from it. + * @tparam Func Callable type that accepts an rvalue LockedPtr and returns a score::cpp::optional specialization. + * @param f The callable to apply. + * @return The result of f(std::move(*this)) if non-null, score::cpp::nullopt otherwise. + */ + template >, + typename Ret = std::invoke_result_t, + typename = std::enable_if_t>> + [[nodiscard]] auto and_then(Func&& f) && -> Ret + { + if (ptr_ == nullptr) + { + return score::cpp::nullopt; + } + return std::invoke(std::forward(f), std::move(*this)); + } + + /** + * @brief Monadic bind. Applies a callable that returns a score::cpp::optional. + * If the pointer is null, short-circuits to score::cpp::nullopt without invoking the callable. + * The callable receives a const lvalue reference to this LockedPtr (from a const rvalue). + * @tparam Func Callable type that accepts const LockedPtr& and returns a score::cpp::optional specialization. + * @param f The callable to apply. + * @return The result of f(*this) if non-null, score::cpp::nullopt otherwise. + */ + template >, + typename Ret = std::invoke_result_t, + typename = std::enable_if_t>> + [[nodiscard]] auto and_then(Func&& f) const&& -> Ret + { + if (ptr_ == nullptr) + { + return score::cpp::nullopt; + } + return std::invoke(std::forward(f), *this); + } + /** * @brief Bool conversion operator checks if the LockedPtr is managing a valid pointer. * @return true if the LockedPtr is managing a non-null pointer, false otherwise. diff --git a/score/concurrency/locked_ptr_test.cpp b/score/concurrency/locked_ptr_test.cpp index b2604aeee1..454ff7d289 100644 --- a/score/concurrency/locked_ptr_test.cpp +++ b/score/concurrency/locked_ptr_test.cpp @@ -41,6 +41,39 @@ struct IntWrapper }; using LPtr2IntW = LockedPtr>; + +score::cpp::optional OptValueBy10(LPtr2IntW& lp) +{ + return lp->value / 10.0; +} + +score::cpp::optional COptValueBy10(const LPtr2IntW& lp) +{ + return lp->value / 10.0; +} + +score::cpp::optional ValueIfPositive(LPtr2IntW& lp) +{ + if (lp->value > 0) + { + return lp->value; + } + return score::cpp::nullopt; +} + +score::cpp::optional CValueIfPositive(const LPtr2IntW& lp) +{ + if (lp->value > 0) + { + return lp->value; + } + return score::cpp::nullopt; +} + +score::cpp::optional OptMoveGet(LPtr2IntW lp) +{ + return lp.get(); +} } // namespace TEST(LockedPtrTest, ConstructionWithTypes) @@ -454,4 +487,121 @@ TEST(LockedPtrTest, UnlockGuard) EXPECT_TRUE(mut.is_locked()); } +TEST(LockedPtrTest, AndThenLvalueRefNotNull) +{ + IntWrapper obj{42}; + MockMutex mut; + LockedPtr lockedptr(&obj, std::unique_lock{mut}); + + EXPECT_EQ(lockedptr.and_then(OptValueBy10), score::cpp::optional{4.2}); + EXPECT_EQ(lockedptr.and_then(COptValueBy10), score::cpp::optional{4.2}); + + auto result = lockedptr.and_then(OptValueBy10); + ASSERT_TRUE((std::is_same_v>)) + << "and_then should return score::cpp::optional"; +} + +TEST(LockedPtrTest, AndThenLvalueRefNull) +{ + IntWrapper* nullp = nullptr; + MockMutex mut; + auto lp = LockedPtr(nullp, std::unique_lock{mut}); + + EXPECT_EQ(lp.and_then(OptValueBy10), score::cpp::nullopt); + EXPECT_EQ(lp.and_then(COptValueBy10), score::cpp::nullopt); +} + +TEST(LockedPtrTest, AndThenLvalueRefCallableReturnsNullopt) +{ + IntWrapper obj{-5}; + MockMutex mut; + LockedPtr lockedptr(&obj, std::unique_lock{mut}); + + EXPECT_EQ(lockedptr.and_then(ValueIfPositive), score::cpp::nullopt); + + obj.value = 10; + EXPECT_EQ(lockedptr.and_then(ValueIfPositive), score::cpp::optional{10}); +} + +TEST(LockedPtrTest, AndThenConstLvalueRefNotNull) +{ + IntWrapper obj{42}; + MockMutex mut; + const auto lockedptr = LockedPtr(&obj, std::unique_lock{mut}); + + EXPECT_EQ(lockedptr.and_then(COptValueBy10), score::cpp::optional{4.2}); + + auto result = lockedptr.and_then(COptValueBy10); + ASSERT_TRUE((std::is_same_v>)) + << "and_then should return score::cpp::optional"; +} + +TEST(LockedPtrTest, AndThenConstLvalueRefNull) +{ + IntWrapper* nullp = nullptr; + MockMutex mut; + const auto lp = LockedPtr(nullp, std::unique_lock{mut}); + + EXPECT_EQ(lp.and_then(COptValueBy10), score::cpp::nullopt); +} + +TEST(LockedPtrTest, AndThenConstLvalueRefCallableReturnsNullopt) +{ + IntWrapper obj{-5}; + MockMutex mut; + const auto lockedptr = LockedPtr(&obj, std::unique_lock{mut}); + + EXPECT_EQ(lockedptr.and_then(CValueIfPositive), score::cpp::nullopt); + + obj.value = 10; + EXPECT_EQ(lockedptr.and_then(CValueIfPositive), score::cpp::optional{10}); +} + +TEST(LockedPtrTest, AndThenRvalueRefNotNull) +{ + IntWrapper obj{42}; + MockMutex mut; + auto lp = LockedPtr(&obj, std::unique_lock{mut}); + + auto result = std::move(lp).and_then(OptMoveGet); + + ASSERT_TRUE((std::is_same_v>)) + << "and_then should return score::cpp::optional"; + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), &obj); + EXPECT_FALSE(lp); +} + +TEST(LockedPtrTest, AndThenRvalueRefNull) +{ + IntWrapper* nullp = nullptr; + MockMutex mut; + + auto result = LockedPtr(nullp, std::unique_lock{mut}).and_then(OptMoveGet); + + EXPECT_EQ(result, score::cpp::nullopt); +} + +TEST(LockedPtrTest, AndThenConstRvalueRefNotNull) +{ + IntWrapper obj{42}; + MockMutex mut; + const auto lp = LockedPtr(&obj, std::unique_lock{mut}); + + auto result = std::move(lp).and_then(COptValueBy10); + + ASSERT_TRUE((std::is_same_v>)) + << "and_then should return score::cpp::optional"; + EXPECT_EQ(result, score::cpp::optional{4.2}); +} + +TEST(LockedPtrTest, AndThenConstRvalueRefNull) +{ + IntWrapper* nullp = nullptr; + MockMutex mut; + const auto lp = LockedPtr(nullp, std::unique_lock{mut}); + + EXPECT_EQ(std::move(lp).and_then(COptValueBy10), score::cpp::nullopt); +} + } // namespace test diff --git a/score/concurrency/type_traits.h b/score/concurrency/type_traits.h index b339feb2ca..8e071377e1 100644 --- a/score/concurrency/type_traits.h +++ b/score/concurrency/type_traits.h @@ -13,6 +13,8 @@ #ifndef SCORE_LIB_CONCURRENCY_TYPE_TRAITS_H #define SCORE_LIB_CONCURRENCY_TYPE_TRAITS_H +#include + #include namespace score