Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions score/concurrency/locked_ptr.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "score/concurrency/type_traits.h"
#include "score/concurrency/unlock_guard.h"

#include <functional>
#include <utility>

namespace score
Expand Down Expand Up @@ -192,6 +193,90 @@ class LockedPtr
return UnlockGuard<Lock>{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 Func,
typename = std::enable_if_t<std::is_invocable_v<Func, LockedPtr&>>,
typename Ret = std::invoke_result_t<Func, LockedPtr&>,
Comment thread
sankurm marked this conversation as resolved.
typename = std::enable_if_t<is_optional<Ret>::value>>
[[nodiscard]] auto and_then(Func&& f) & -> Ret
{
if (ptr_ == nullptr)
{
return score::cpp::nullopt;
}
return std::invoke(std::forward<Func>(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 Func,
typename = std::enable_if_t<std::is_invocable_v<Func, const LockedPtr&>>,
typename Ret = std::invoke_result_t<Func, const LockedPtr&>,
typename = std::enable_if_t<is_optional<Ret>::value>>
[[nodiscard]] auto and_then(Func&& f) const& -> Ret
{
if (ptr_ == nullptr)
{
return score::cpp::nullopt;
}
return std::invoke(std::forward<Func>(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 LockedPtr is moved into the callable by value, transferring lock ownership.
* @tparam Func Callable type that accepts LockedPtr by value and returns a score::cpp::optional specialization.
Comment thread
sankurm marked this conversation as resolved.
Outdated
* @param f The callable to apply.
* @return The result of f(std::move(*this)) if non-null, score::cpp::nullopt otherwise.
*/
template <typename Func,
typename = std::enable_if_t<std::is_invocable_v<Func, LockedPtr>>,
typename Ret = std::invoke_result_t<Func, LockedPtr>,
typename = std::enable_if_t<is_optional<Ret>::value>>
[[nodiscard]] auto and_then(Func&& f) && -> Ret
{
if (ptr_ == nullptr)
{
return score::cpp::nullopt;
}
return std::invoke(std::forward<Func>(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 Func,
typename = std::enable_if_t<std::is_invocable_v<Func, const LockedPtr&>>,
typename Ret = std::invoke_result_t<Func, const LockedPtr&>,
typename = std::enable_if_t<is_optional<Ret>::value>>
[[nodiscard]] auto and_then(Func&& f) const&& -> Ret
{
if (ptr_ == nullptr)
{
return score::cpp::nullopt;
}
return std::invoke(std::forward<Func>(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.
Expand Down
145 changes: 145 additions & 0 deletions score/concurrency/locked_ptr_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ struct IntWrapper
};

using LPtr2IntW = LockedPtr<IntWrapper, std::unique_lock<MockMutex>>;

auto opt_value_by_10 = [](LPtr2IntW& lp) -> score::cpp::optional<double> {
return lp->value / 10.0;
};

auto copt_value_by_10 = [](const LPtr2IntW& lp) -> score::cpp::optional<double> {
Comment thread
sankurm marked this conversation as resolved.
Outdated
return lp->value / 10.0;
};

auto value_if_positive = [](LPtr2IntW& lp) -> score::cpp::optional<int> {
if (lp->value > 0)
{
return lp->value;
}
return score::cpp::nullopt;
};

auto cvalue_if_positive = [](const LPtr2IntW& lp) -> score::cpp::optional<int> {
if (lp->value > 0)
{
return lp->value;
}
return score::cpp::nullopt;
};

auto opt_move_get = [](LPtr2IntW lp) -> score::cpp::optional<IntWrapper*> {
return lp.get();
};
} // namespace

TEST(LockedPtrTest, ConstructionWithTypes)
Expand Down Expand Up @@ -454,4 +482,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(opt_value_by_10), score::cpp::optional{4.2});
EXPECT_EQ(lockedptr.and_then(copt_value_by_10), score::cpp::optional{4.2});

auto result = lockedptr.and_then(opt_value_by_10);
ASSERT_TRUE((std::is_same_v<decltype(result), score::cpp::optional<double>>))
<< "and_then should return score::cpp::optional<double>";
}

TEST(LockedPtrTest, AndThenLvalueRefNull)
{
IntWrapper* nullp = nullptr;
MockMutex mut;
auto lp = LockedPtr(nullp, std::unique_lock{mut});

EXPECT_EQ(lp.and_then(opt_value_by_10), score::cpp::nullopt);
EXPECT_EQ(lp.and_then(copt_value_by_10), score::cpp::nullopt);
}

TEST(LockedPtrTest, AndThenLvalueRefCallableReturnsNullopt)
{
IntWrapper obj{-5};
MockMutex mut;
LockedPtr lockedptr(&obj, std::unique_lock{mut});

EXPECT_EQ(lockedptr.and_then(value_if_positive), score::cpp::nullopt);

obj.value = 10;
EXPECT_EQ(lockedptr.and_then(value_if_positive), 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(copt_value_by_10), score::cpp::optional{4.2});

auto result = lockedptr.and_then(copt_value_by_10);
ASSERT_TRUE((std::is_same_v<decltype(result), score::cpp::optional<double>>))
<< "and_then should return score::cpp::optional<double>";
}

TEST(LockedPtrTest, AndThenConstLvalueRefNull)
{
IntWrapper* nullp = nullptr;
MockMutex mut;
const auto lp = LockedPtr(nullp, std::unique_lock{mut});

EXPECT_EQ(lp.and_then(copt_value_by_10), 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(cvalue_if_positive), score::cpp::nullopt);

obj.value = 10;
EXPECT_EQ(lockedptr.and_then(cvalue_if_positive), 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(opt_move_get);

ASSERT_TRUE((std::is_same_v<decltype(result), score::cpp::optional<IntWrapper*>>))
<< "and_then should return score::cpp::optional<IntWrapper*>";
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(opt_move_get);

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(copt_value_by_10);

ASSERT_TRUE((std::is_same_v<decltype(result), score::cpp::optional<double>>))
<< "and_then should return score::cpp::optional<double>";
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(copt_value_by_10), score::cpp::nullopt);
}

} // namespace test
12 changes: 12 additions & 0 deletions score/concurrency/type_traits.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#ifndef SCORE_LIB_CONCURRENCY_TYPE_TRAITS_H
#define SCORE_LIB_CONCURRENCY_TYPE_TRAITS_H

#include <score/optional.hpp>

#include <type_traits>

namespace score
Expand All @@ -34,6 +36,16 @@ inline constexpr bool is_basic_lockable_v = is_basic_lockable<T>::value;
template <typename T>
using is_basic_lockable_t = typename is_basic_lockable<T>::type;

template <typename>
struct is_optional : std::false_type
{
};

template <typename U>
struct is_optional<score::cpp::optional<U>> : std::true_type
Comment thread
sankurm marked this conversation as resolved.
Outdated
{
};

} // namespace score

#endif // SCORE_LIB_CONCURRENCY_TYPE_TRAITS_H
Loading