Topic/optional monadic operations (#1501)

* Topic/optional monadic operations

* topic/optional-monadic-operations:
	- added and_then, or_else, and transform to etl::optional with unit tests
	- added is_optional to optional.h, used in and_then and or_else to ensure that the returned type is an optional
	- implemented as ref-qualified overloads with C++11 compatible trailing return types, following the etl::expected implementation

* Convert to C++11 style type traits

* Coderabbit changes

Constrain transform before forming etl::optional<U>.
Remove references from monadic result types.

* clang-format updates

---------

Co-authored-by: John Wellbelove <john.wellbelove@etlcpp.com>
Co-authored-by: John Wellbelove <jwellbelove@users.noreply.github.com>
This commit is contained in:
davmf 2026-08-25 18:29:01 +10:00 committed by GitHub
parent 01a07a1728
commit f7520ad0bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 374 additions and 42 deletions

View File

@ -36,6 +36,7 @@ SOFTWARE.
#include "error_handler.h"
#include "exception.h"
#include "initializer_list.h"
#include "invoke.h"
#include "memory.h"
#include "placement_new.h"
#include "type_traits.h"
@ -49,6 +50,16 @@ namespace etl
template <typename T>
class optional;
template <typename T>
struct is_optional : etl::false_type
{
};
template <typename T>
struct is_optional<optional<T> > : etl::true_type
{
};
//*****************************************************************************
/// A null option type.
///\ingroup utilities
@ -174,10 +185,9 @@ namespace etl
/// Constructs T in-place from U&&, without requiring T to be
/// copy/move constructible.
//***************************************************************************
template <typename U,
typename etl::enable_if< etl::is_constructible<T, U&&>::value && !etl::is_same<typename etl::decay<U>::type, etl::in_place_t>::value
&& !etl::is_same<typename etl::decay<U>::type, optional_impl>::value,
int>::type = 0>
template <typename U, etl::enable_if_t< etl::is_constructible<T, U&&>::value && !etl::is_same<etl::decay_t<U>, etl::in_place_t>::value
&& !etl::is_same<etl::decay_t<U>, optional_impl>::value,
int> = 0>
ETL_CONSTEXPR20_STL optional_impl(U&& value_) ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, U&&>::value))
{
storage.construct(etl::forward<U>(value_));
@ -276,9 +286,7 @@ namespace etl
/// Assignment operator from value type.
//***************************************************************************
#if ETL_USING_CPP11
template <typename U,
typename etl::enable_if< etl::is_constructible<T, U&&>::value && !etl::is_same<typename etl::decay<U>::type, optional_impl>::value,
int>::type = 0>
template <typename U, etl::enable_if_t< etl::is_constructible<T, U&&>::value && !etl::is_same<etl::decay_t<U>, optional_impl>::value, int> = 0>
ETL_CONSTEXPR20_STL optional_impl& operator=(U&& value_) ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, U&&>::value))
{
storage.construct(etl::forward<U>(value_));
@ -511,8 +519,7 @@ namespace etl
//*******************************************
/// Emplaces a value from initializer_list and arbitrary constructor arguments.
//*******************************************
template <typename U, typename... Args,
typename etl::enable_if< etl::is_constructible<T, std::initializer_list<U>&, Args...>::value, int>::type = 0 >
template <typename U, typename... Args, etl::enable_if_t< etl::is_constructible<T, std::initializer_list<U>&, Args...>::value, int> = 0 >
ETL_CONSTEXPR20_STL T& emplace(std::initializer_list<U> ilist, Args&&... args)
ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, std::initializer_list<U>&, Args...>::value))
{
@ -1217,8 +1224,8 @@ namespace etl
};
} // namespace private_optional
#define ETL_OPTIONAL_ENABLE_CPP14 typename etl::enable_if< etl::is_pod<typename etl::remove_cv<U>::type>::value, int>::type = 0
#define ETL_OPTIONAL_ENABLE_CPP20_STL typename etl::enable_if< !etl::is_pod<typename etl::remove_cv<U>::type>::value, int>::type = 0
#define ETL_OPTIONAL_ENABLE_CPP14 etl::enable_if_t< etl::is_pod<typename etl::remove_cv<U>::type>::value, int> = 0
#define ETL_OPTIONAL_ENABLE_CPP20_STL etl::enable_if_t< !etl::is_pod<typename etl::remove_cv<U>::type>::value, int> = 0
#define ETL_OPTIONAL_ENABLE_CONSTEXPR_BOOL_RETURN_CPP14 \
ETL_CONSTEXPR14 typename etl::enable_if< etl::is_pod<typename etl::remove_cv<T>::type>::value, bool>::type
@ -1319,12 +1326,10 @@ namespace etl
/// Constructs T in-place from U&&, without requiring T to be
/// copy/move constructible.
//***************************************************************************
template < typename U,
typename etl::enable_if< etl::is_constructible<T, U&&>::value && !etl::is_same<typename etl::decay<U>::type, etl::optional<T>>::value
&& !etl::is_same<typename etl::decay<U>::type, etl::in_place_t>::value
&& !etl::is_same<typename etl::decay<U>::type, etl::nullopt_t>::value
&& etl::is_pod<typename etl::remove_cv<T>::type>::value,
int>::type = 0>
template < typename U, etl::enable_if_t< etl::is_constructible<T, U&&>::value && !etl::is_same<etl::decay_t<U>, etl::optional<T>>::value
&& !etl::is_same<etl::decay_t<U>, etl::in_place_t>::value
&& !etl::is_same<etl::decay_t<U>, etl::nullopt_t>::value && etl::is_pod<etl::remove_cvref_t<T>>::value,
int> = 0>
ETL_CONSTEXPR14 optional(U&& value_) ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, U&&>::value))
: impl_t(etl::forward<U>(value_))
{
@ -1334,11 +1339,10 @@ namespace etl
/// Converting constructor from value type.
//***************************************************************************
template < typename U,
typename etl::enable_if< etl::is_constructible<T, U&&>::value && !etl::is_same<typename etl::decay<U>::type, etl::optional<T>>::value
&& !etl::is_same<typename etl::decay<U>::type, etl::in_place_t>::value
&& !etl::is_same<typename etl::decay<U>::type, etl::nullopt_t>::value
&& !etl::is_pod<typename etl::remove_cv<T>::type>::value,
int>::type = 0>
etl::enable_if_t< etl::is_constructible<T, U&&>::value && !etl::is_same<etl::decay_t<U>, etl::optional<T>>::value
&& !etl::is_same<etl::decay_t<U>, etl::in_place_t>::value && !etl::is_same<etl::decay_t<U>, etl::nullopt_t>::value
&& !etl::is_pod<etl::remove_cvref_t<T>>::value,
int> = 0>
ETL_CONSTEXPR20_STL optional(U&& value_) ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, U&&>::value))
: impl_t(etl::forward<U>(value_))
{
@ -1446,11 +1450,9 @@ namespace etl
//***************************************************************************
/// Converting assignment operator from value type.
//***************************************************************************
template < typename U,
typename etl::enable_if< etl::is_constructible<T, U&&>::value && !etl::is_same<typename etl::decay<U>::type, etl::optional<T>>::value
&& !etl::is_same<typename etl::decay<U>::type, etl::nullopt_t>::value
&& etl::is_pod<typename etl::remove_cv<T>::type>::value,
int>::type = 0>
template < typename U, etl::enable_if_t< etl::is_constructible<T, U&&>::value && !etl::is_same<etl::decay_t<U>, etl::optional<T>>::value
&& !etl::is_same<etl::decay_t<U>, etl::nullopt_t>::value && etl::is_pod<etl::remove_cvref_t<T>>::value,
int> = 0>
ETL_CONSTEXPR14 optional& operator=(U&& value_) ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, U&&>::value))
{
impl_t::operator=(etl::forward<U>(value_));
@ -1462,10 +1464,9 @@ namespace etl
/// Converting assignment operator from value type.
//***************************************************************************
template < typename U,
typename etl::enable_if< etl::is_constructible<T, U&&>::value && !etl::is_same<typename etl::decay<U>::type, etl::optional<T>>::value
&& !etl::is_same<typename etl::decay<U>::type, etl::nullopt_t>::value
&& !etl::is_pod<typename etl::remove_cv<T>::type>::value,
int>::type = 0>
etl::enable_if_t< etl::is_constructible<T, U&&>::value && !etl::is_same<etl::decay_t<U>, etl::optional<T>>::value
&& !etl::is_same<etl::decay_t<U>, etl::nullopt_t>::value && !etl::is_pod<etl::remove_cvref_t<T>>::value,
int> = 0>
ETL_CONSTEXPR20_STL optional& operator=(U&& value_) ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, U&&>::value))
{
impl_t::operator=(etl::forward<U>(value_));
@ -1519,6 +1520,124 @@ namespace etl
{
return this->has_value() ? this->operator->() + 1 : ETL_NULLPTR;
}
#if ETL_USING_CPP11
template <typename F, typename U = etl::remove_cvref_t<etl::invoke_result_t<F, T&>>, etl::enable_if_t<!etl::is_void<U>::value, int> = 0>
auto transform(F&& f) & -> etl::optional<U>
{
return transform_impl<F, optional&, U, T&>(etl::forward<F>(f), *this);
}
template <typename F, typename U = etl::remove_cvref_t<etl::invoke_result_t<F, const T&>>, etl::enable_if_t<!etl::is_void<U>::value, int> = 0>
auto transform(F&& f) const& -> etl::optional<U>
{
return transform_impl<F, const optional&, U, const T&>(etl::forward<F>(f), *this);
}
template <typename F, typename U = etl::remove_cvref_t<etl::invoke_result_t<F, T&&>>, etl::enable_if_t<!etl::is_void<U>::value, int> = 0>
auto transform(F&& f) && -> etl::optional<U>
{
return transform_impl<F, optional&&, U, T&&>(etl::forward<F>(f), etl::move(*this));
}
template <typename F, typename U = etl::remove_cvref_t<etl::invoke_result_t<F, const T&&>>, etl::enable_if_t<!etl::is_void<U>::value, int> = 0>
auto transform(F&& f) const&& -> etl::optional<U>
{
return transform_impl<F, const optional&&, U, const T&&>(etl::forward<F>(f), etl::move(*this));
}
template <typename F, typename U = etl::remove_cvref_t< etl::invoke_result_t<F, T&>>>
auto and_then(F&& f) & -> U
{
return and_then_impl<F, optional&, U, T&>(etl::forward<F>(f), *this);
}
template < typename F, typename U = etl::remove_cvref_t< etl::invoke_result_t<F, const T&>>>
auto and_then(F&& f) const& -> U
{
return and_then_impl<F, const optional&, U, const T&>(etl::forward<F>(f), *this);
}
template <typename F, typename U = etl::remove_cvref_t< etl::invoke_result_t<F, T&&>>>
auto and_then(F&& f) && -> U
{
return and_then_impl<F, optional&&, U, T&&>(etl::forward<F>(f), etl::move(*this));
}
template < typename F, typename U = etl::remove_cvref_t< etl::invoke_result_t<F, const T&&>>>
auto and_then(F&& f) const&& -> U
{
return and_then_impl<F, const optional&&, U, const T&&>(etl::forward<F>(f), etl::move(*this));
}
template <typename F, typename U = etl::remove_cvref_t<etl::invoke_result_t<F>>>
auto or_else(F&& f) & -> U
{
return or_else_impl<F, optional&, U>(etl::forward<F>(f), *this);
}
template < typename F, typename U = etl::remove_cvref_t<etl::invoke_result_t<F>>>
auto or_else(F&& f) const& -> U
{
return or_else_impl<F, const optional&, U>(etl::forward<F>(f), *this);
}
template <typename F, typename U = etl::remove_cvref_t< etl::invoke_result_t<F>>>
auto or_else(F&& f) && -> U
{
return or_else_impl<F, optional&&, U>(etl::forward<F>(f), etl::move(*this));
}
template < typename F, typename U = etl::remove_cvref_t< etl::invoke_result_t<F>>>
auto or_else(F&& f) const&& -> U
{
return or_else_impl<F, const optional&&, U>(etl::forward<F>(f), etl::move(*this));
}
private:
template < typename F, typename TOpt, typename TRet, typename TValueRef, typename = etl::enable_if_t<!etl::is_void<TRet>::value>>
auto transform_impl(F&& f, TOpt&& opt) const -> etl::optional<TRet>
{
if (opt.has_value())
{
return etl::optional<TRet>(etl::invoke(etl::forward<F>(f), etl::forward<TValueRef>(*opt)));
}
else
{
return etl::optional<TRet>();
}
}
template < typename F, typename TOpt, typename TRet, typename TValueRef,
typename = etl::enable_if_t<!etl::is_void<TRet>::value && etl::is_optional<TRet>::value>>
auto and_then_impl(F&& f, TOpt&& opt) const -> TRet
{
if (opt.has_value())
{
return etl::invoke(etl::forward<F>(f), etl::forward<TValueRef>(*opt));
}
else
{
return TRet();
}
}
template <
typename F, typename TOpt, typename TRet,
typename = etl::enable_if_t< !etl::is_void<TRet>::value && etl::is_optional<TRet>::value && etl::is_same<typename TRet::value_type, T>::value>>
auto or_else_impl(F&& f, TOpt&& opt) const -> TRet
{
if (opt.has_value())
{
return TRet(etl::forward<TOpt>(opt));
}
else
{
return etl::invoke(etl::forward<F>(f));
}
}
#endif
};
#include "private/diagnostic_uninitialized_push.h"
@ -2132,17 +2251,17 @@ namespace etl
/// This overload participates in overload resolution only if
/// `etl::is_constructible_v<T, Args...>` is true.
//***************************************************************************
template <typename T, typename... Args, //
typename etl::enable_if< etl::is_constructible<T, Args...>::value, int>::type = 0, //
template <typename T, typename... Args, //
etl::enable_if_t< etl::is_constructible<T, Args...>::value, int> = 0, //
typename U = T, ETL_OPTIONAL_ENABLE_CPP14>
ETL_CONSTEXPR14 etl::optional<T> make_optional(Args&&... args) //
ETL_CONSTEXPR14 etl::optional<T> make_optional(Args&&... args) //
ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, Args...>::value))
{
return etl::optional<T>(etl::in_place_t{}, etl::forward<Args>(args)...);
}
template <typename T, typename... Args, //
typename etl::enable_if< etl::is_constructible<T, Args...>::value, int>::type = 0, //
typename U = T, ETL_OPTIONAL_ENABLE_CPP20_STL>
template <typename T, typename... Args, //
etl::enable_if_t< etl::is_constructible<T, Args...>::value, int> = 0, //
typename U = T, ETL_OPTIONAL_ENABLE_CPP20_STL>
ETL_CONSTEXPR20_STL etl::optional<T> make_optional(Args&&... args) //
ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, Args...>::value))
{
@ -2156,17 +2275,17 @@ namespace etl
/// This overload participates in overload resolution only if
/// `etl::is_constructible_v<T, std::initializer_list<U>&, Args...>` is true.
//***************************************************************************
template <typename T, typename TL, typename... Args, //
typename etl::enable_if< etl::is_constructible<T, std::initializer_list<TL>&, Args...>::value, int>::type = 0, //
template <typename T, typename TL, typename... Args, //
etl::enable_if_t< etl::is_constructible<T, std::initializer_list<TL>&, Args...>::value, int> = 0, //
typename U = T, ETL_OPTIONAL_ENABLE_CPP14>
ETL_CONSTEXPR14 etl::optional<T> make_optional(std::initializer_list<TL> ilist, Args&&... args)
ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, std::initializer_list<TL>&, Args...>::value))
{
return etl::optional<T>(etl::in_place_t{}, ilist, etl::forward<Args>(args)...);
}
template <typename T, typename TL, typename... Args, //
typename etl::enable_if< etl::is_constructible<T, std::initializer_list<TL>&, Args...>::value, int>::type = 0, //
typename U = T, ETL_OPTIONAL_ENABLE_CPP20_STL>
template <typename T, typename TL, typename... Args, //
etl::enable_if_t< etl::is_constructible<T, std::initializer_list<TL>&, Args...>::value, int> = 0, //
typename U = T, ETL_OPTIONAL_ENABLE_CPP20_STL>
ETL_CONSTEXPR20_STL etl::optional<T> make_optional(std::initializer_list<TL> ilist, Args&&... args)
ETL_NOEXCEPT_IF((etl::is_nothrow_constructible<T, std::initializer_list<TL>&, Args...>::value))
{

View File

@ -1603,5 +1603,218 @@ namespace
#endif
}
#endif
#if ETL_USING_CPP11
//*************************************************************************
TEST(test_transform)
{
etl::optional<int> engaged(42);
etl::optional<int> empty;
etl::optional<int> doubled = engaged.transform([](int i) { return i * 2; });
CHECK_TRUE(doubled.has_value());
CHECK_EQUAL(84, doubled.value());
etl::optional<int> from_empty = empty.transform([](int i) { return i * 2; });
CHECK_FALSE(from_empty.has_value());
}
//*************************************************************************
TEST(test_transform_const)
{
const etl::optional<int> engaged(42);
const etl::optional<int> empty;
etl::optional<int> doubled = engaged.transform([](int i) { return i * 2; });
CHECK_TRUE(doubled.has_value());
CHECK_EQUAL(84, doubled.value());
etl::optional<int> from_empty = empty.transform([](int i) { return i * 2; });
CHECK_FALSE(from_empty.has_value());
}
//*************************************************************************
TEST(test_transform_change_type)
{
etl::optional<int> engaged(42);
etl::optional<int> empty;
etl::optional<std::string> text = engaged.transform([](int) { return std::string("forty two"); });
CHECK_TRUE(text.has_value());
CHECK_EQUAL("forty two", text.value());
etl::optional<std::string> text_from_empty = empty.transform([](int) { return std::string("forty two"); });
CHECK_FALSE(text_from_empty.has_value());
}
//*************************************************************************
TEST(test_transform_non_pod)
{
etl::optional<Data> engaged(Data("transform"));
etl::optional<Data> empty;
etl::optional<size_t> length = engaged.transform([](const Data& d) { return d.value.size(); });
CHECK_TRUE(length.has_value());
CHECK_EQUAL(9U, length.value());
etl::optional<size_t> length_from_empty = empty.transform([](const Data& d) { return d.value.size(); });
CHECK_FALSE(length_from_empty.has_value());
}
//*************************************************************************
TEST(test_transform_move)
{
etl::optional<DataM> engaged(DataM(42U));
etl::optional<DataM> moved = etl::move(engaged).transform([](DataM&& d) { return DataM(etl::move(d)); });
CHECK_TRUE(moved.has_value());
CHECK_TRUE(moved.value().valid);
CHECK_EQUAL(42U, moved.value().value);
CHECK_FALSE(engaged.value().valid);
etl::optional<DataM> empty;
etl::optional<DataM> from_empty = etl::move(empty).transform([](DataM&& d) { return DataM(etl::move(d)); });
CHECK_FALSE(from_empty.has_value());
}
//*************************************************************************
TEST(test_and_then)
{
etl::optional<int> engaged(42);
etl::optional<int> empty;
etl::optional<int> incremented = engaged.and_then([](int i) { return etl::optional<int>(i + 1); });
CHECK_TRUE(incremented.has_value());
CHECK_EQUAL(43, incremented.value());
etl::optional<int> from_empty = empty.and_then([](int i) { return etl::optional<int>(i + 1); });
CHECK_FALSE(from_empty.has_value());
etl::optional<int> rejected = engaged.and_then([](int) { return etl::optional<int>(); });
CHECK_FALSE(rejected.has_value());
}
//*************************************************************************
TEST(test_and_then_const)
{
const etl::optional<int> engaged(42);
const etl::optional<int> empty;
etl::optional<int> incremented = engaged.and_then([](int i) { return etl::optional<int>(i + 1); });
CHECK_TRUE(incremented.has_value());
CHECK_EQUAL(43, incremented.value());
etl::optional<int> from_empty = empty.and_then([](int i) { return etl::optional<int>(i + 1); });
CHECK_FALSE(from_empty.has_value());
}
//*************************************************************************
TEST(test_and_then_change_type)
{
etl::optional<int> engaged(42);
etl::optional<int> empty;
etl::optional<std::string> text = engaged.and_then([](int) { return etl::optional<std::string>("forty two"); });
CHECK_TRUE(text.has_value());
CHECK_EQUAL("forty two", text.value());
etl::optional<std::string> text_from_empty = empty.and_then([](int) { return etl::optional<std::string>("forty two"); });
CHECK_FALSE(text_from_empty.has_value());
}
//*************************************************************************
TEST(test_and_then_move)
{
etl::optional<DataM> engaged(DataM(42U));
etl::optional<uint32_t> value = etl::move(engaged).and_then([](DataM&& d) { return etl::optional<uint32_t>(d.value); });
CHECK_TRUE(value.has_value());
CHECK_EQUAL(42U, value.value());
etl::optional<DataM> empty;
etl::optional<uint32_t> from_empty = etl::move(empty).and_then([](DataM&& d) { return etl::optional<uint32_t>(d.value); });
CHECK_FALSE(from_empty.has_value());
}
//*************************************************************************
TEST(test_or_else)
{
etl::optional<int> engaged(42);
etl::optional<int> empty;
bool called = false;
etl::optional<int> from_engaged = engaged.or_else(
[&called]()
{
called = true;
return etl::optional<int>(99);
});
CHECK_TRUE(from_engaged.has_value());
CHECK_EQUAL(42, from_engaged.value());
CHECK_FALSE(called);
etl::optional<int> from_empty = empty.or_else(
[&called]()
{
called = true;
return etl::optional<int>(99);
});
CHECK_TRUE(from_empty.has_value());
CHECK_EQUAL(99, from_empty.value());
CHECK_TRUE(called);
etl::optional<int> still_empty = empty.or_else([]() { return etl::optional<int>(); });
CHECK_FALSE(still_empty.has_value());
}
//*************************************************************************
TEST(test_or_else_const)
{
const etl::optional<int> engaged(42);
const etl::optional<int> empty;
etl::optional<int> from_engaged = engaged.or_else([]() { return etl::optional<int>(99); });
CHECK_TRUE(from_engaged.has_value());
CHECK_EQUAL(42, from_engaged.value());
etl::optional<int> from_empty = empty.or_else([]() { return etl::optional<int>(99); });
CHECK_TRUE(from_empty.has_value());
CHECK_EQUAL(99, from_empty.value());
}
//*************************************************************************
TEST(test_or_else_move)
{
etl::optional<DataM> engaged(DataM(42U));
etl::optional<DataM> from_engaged = etl::move(engaged).or_else([]() { return etl::optional<DataM>(DataM(99U)); });
CHECK_TRUE(from_engaged.has_value());
CHECK_EQUAL(42U, from_engaged.value().value);
etl::optional<DataM> empty;
etl::optional<DataM> from_empty = etl::move(empty).or_else([]() { return etl::optional<DataM>(DataM(99U)); });
CHECK_TRUE(from_empty.has_value());
CHECK_EQUAL(99U, from_empty.value().value);
}
//*************************************************************************
TEST(test_monadic_chaining)
{
etl::optional<int> engaged(8);
etl::optional<int> empty;
etl::optional<int> result = engaged.transform([](int i) { return i * 2; })
.and_then([](int i) { return etl::optional<int>(i + 1); })
.or_else([]() { return etl::optional<int>(0); });
CHECK_TRUE(result.has_value());
CHECK_EQUAL(17, result.value());
etl::optional<int> result_from_empty = empty.transform([](int i) { return i * 2; })
.and_then([](int i) { return etl::optional<int>(i + 1); })
.or_else([]() { return etl::optional<int>(123); });
CHECK_TRUE(result_from_empty.has_value());
CHECK_EQUAL(123, result_from_empty.value());
}
#endif
}
} // namespace