diff --git a/universal/include/userver/utils/expected.hpp b/universal/include/userver/utils/expected.hpp index 1c948f241a91..b1ecdc5cb143 100644 --- a/universal/include/userver/utils/expected.hpp +++ b/universal/include/userver/utils/expected.hpp @@ -3,8 +3,8 @@ /// @file userver/utils/expected.hpp /// @brief @copybrief utils::expected -#include #include +#include #include #include @@ -79,7 +79,7 @@ class [[nodiscard]] expected { /// @brief Return reference to the value or throws bad_expected_access /// if it's not available - /// @throws utils::bad_expected_access if *this contain an unexpected value + /// @throws utils::bad_expected_access if the value is not available S& value() & USERVER_IMPL_LIFETIME_BOUND; /// @overload @@ -88,9 +88,12 @@ class [[nodiscard]] expected { /// @overload const S& value() const& USERVER_IMPL_LIFETIME_BOUND; + /// @brief Check whether *this contains an error + bool has_error() const noexcept; + /// @brief Return reference to the error value or throws bad_expected_access /// if it's not available - /// @throws utils::bad_expected_access if success value is not available + /// @throws utils::bad_expected_access if the error value is not available E& error() USERVER_IMPL_LIFETIME_BOUND; /// @overload @@ -119,6 +122,7 @@ class [[nodiscard]] expected { explicit operator bool() const noexcept; void value() const; + bool has_error() const noexcept; E& error() USERVER_IMPL_LIFETIME_BOUND; const E& error() const USERVER_IMPL_LIFETIME_BOUND; @@ -128,12 +132,12 @@ class [[nodiscard]] expected { template unexpected::unexpected(const E& error) - : value_{error} + : value_(error) {} template unexpected::unexpected(E&& error) - : value_{std::forward(error)} + : value_(std::move(error)) {} template @@ -165,41 +169,41 @@ constexpr expected::expected() template expected::expected(const S& success) - : data_(success) + : data_(std::in_place_index<0>, success) {} template expected::expected(S&& success) - : data_(std::forward(success)) + : data_(std::in_place_index<0>, std::move(success)) {} template expected::expected(const unexpected& error) - : data_(error.error()) + : data_(std::in_place_index<1>, error.error()) {} template expected::expected(unexpected&& error) - : data_(std::forward>(error.error())) + : data_(std::in_place_index<1>, std::move(error.error())) {} template template requires std::is_convertible_v expected::expected(const unexpected& error) - : data_(utils::unexpected(std::forward(error.error()))) + : data_(std::in_place_index<1>, error.error()) {} template template requires std::is_convertible_v expected::expected(unexpected&& error) - : data_(utils::unexpected(std::forward(error.error()))) + : data_(std::in_place_index<1>, std::move(error.error())) {} template bool expected::has_value() const noexcept { - return std::holds_alternative(data_); + return data_.index() == 0; } template @@ -208,71 +212,66 @@ expected::operator bool() const noexcept { } template - S& expected::value() & USERVER_IMPL_LIFETIME_BOUND { - S* result = std::get_if(&data_); - if (result == nullptr) { - throw bad_expected_access("Trying to get undefined value from utils::expected"); - } - return *result; +S& expected::value() & USERVER_IMPL_LIFETIME_BOUND { + return const_cast(std::as_const(*this).value()); } template - S&& expected::value() && USERVER_IMPL_LIFETIME_BOUND { +S&& expected::value() && USERVER_IMPL_LIFETIME_BOUND { return std::move(value()); } template const S& expected::value() const& USERVER_IMPL_LIFETIME_BOUND { - const S* result = std::get_if(&data_); - if (result == nullptr) { - throw bad_expected_access("Trying to get undefined value from utils::expected"); + if (const auto* result = std::get_if<0>(&data_)) { + return *result; } - return *result; + throw bad_expected_access("Trying to get undefined value from utils::expected"); +} + +template +bool expected::has_error() const noexcept { + return data_.index() == 1; } template E& expected::error() USERVER_IMPL_LIFETIME_BOUND { - auto* result = std::get_if>(&data_); - if (result == nullptr) { - throw bad_expected_access("Trying to get undefined error value from utils::expected"); - } - return result->error(); + return const_cast(std::as_const(*this).error()); } template const E& expected::error() const USERVER_IMPL_LIFETIME_BOUND { - const auto* result = std::get_if>(&data_); - if (result == nullptr) { - throw bad_expected_access("Trying to get undefined error value from utils::expected"); + if (const auto* result = std::get_if<1>(&data_)) { + return result->error(); } - return result->error(); + throw bad_expected_access("Trying to get undefined error value from utils::expected"); } template -constexpr expected::expected() noexcept: data_(std::in_place_index<0>) {} +constexpr expected::expected() noexcept : data_(std::in_place_index<0>) {} template expected::expected(const unexpected& error) - : data_(error.error()) + : data_(std::in_place_index<1>, error.error()) {} template expected::expected(unexpected&& error) - : data_(std::forward>(error.error())) + : data_(std::in_place_index<1>, std::move(error.error())) {} template template requires std::is_convertible_v expected::expected(const unexpected& error) - : data_(utils::unexpected(std::forward(error.error()))) + : data_(std::in_place_index<1>, error.error()) {} template template requires std::is_convertible_v expected::expected(unexpected&& error) - : data_(utils::unexpected(std::forward(error.error()))) + : data_(std::in_place_index<1>, std::move(error.error())) {} template @@ -287,27 +286,28 @@ expected::operator bool() const noexcept { template void expected::value() const { - if (!has_value()) { - throw bad_expected_access("Trying to get undefined value from utils::expected"); + if (has_value()) { + return; } + throw bad_expected_access("Trying to get undefined value from utils::expected"); +} + +template +bool expected::has_error() const noexcept { + return data_.index() == 1; } template E& expected::error() USERVER_IMPL_LIFETIME_BOUND { - auto* result = std::get_if>(&data_); - if (result == nullptr) { - throw bad_expected_access("Trying to get undefined error value from utils::expected"); - } - return result->error(); + return const_cast(std::as_const(*this).error()); } template const E& expected::error() const USERVER_IMPL_LIFETIME_BOUND { - const auto* result = std::get_if>(&data_); - if (result == nullptr) { - throw bad_expected_access("Trying to get undefined error value from utils::expected"); + if (const auto* result = std::get_if<1>(&data_)) { + return result->error(); } - return result->error(); + throw bad_expected_access("Trying to get undefined error value from utils::expected"); } // NOLINTEND(readability-identifier-naming) diff --git a/universal/src/utils/expected_test.cpp b/universal/src/utils/expected_test.cpp index 16817db90e59..d50796c5cffb 100644 --- a/universal/src/utils/expected_test.cpp +++ b/universal/src/utils/expected_test.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -42,8 +43,12 @@ TEST(Expected, ErrorCtor) { EXPECT_FALSE(ei); EXPECT_FALSE(ev.has_value()); EXPECT_FALSE(ev); - EXPECT_EQ(const_cast(ei).error(), "string error"); - EXPECT_EQ(const_cast(ev).error(), "string error"); + ASSERT_TRUE(ei.has_error()); + ASSERT_TRUE(ev.has_error()); + ASSERT_NO_THROW(ei.error()); + ASSERT_NO_THROW(ev.error()); + EXPECT_EQ(std::as_const(ei).error(), "string error"); + EXPECT_EQ(std::as_const(ev).error(), "string error"); ei.error() = "another error"; ev.error() = "one more error"; @@ -51,37 +56,67 @@ TEST(Expected, ErrorCtor) { EXPECT_EQ(ei.error(), "another error"); EXPECT_EQ(ev.error(), "one more error"); - ei = ExpectedInt{utils::unexpected("converted error")}; - ev = ExpectedVoid{utils::unexpected("converted error")}; + auto error2 = utils::unexpected("converted error"); + + ei = ExpectedInt{error2}; + ev = ExpectedVoid{std::move(error2)}; EXPECT_FALSE(ei.has_value()); EXPECT_FALSE(ei); EXPECT_FALSE(ev.has_value()); EXPECT_FALSE(ev); + ASSERT_TRUE(ei.has_error()); + ASSERT_TRUE(ev.has_error()); + ASSERT_NO_THROW(ei.error()); + ASSERT_NO_THROW(ev.error()); EXPECT_EQ(ei.error(), "converted error"); EXPECT_EQ(ev.error(), "converted error"); } -TEST(Expected, ValueThrowsIfExpectedContainsError) { +TEST(Expected, ValueThrowsIfExpectedContainsNoValue) { auto error = utils::unexpected{std::string("string error")}; ExpectedInt ei{error}; ExpectedVoid ev{std::move(error)}; - EXPECT_THROW(const_cast(ei).value(), utils::bad_expected_access); + ASSERT_FALSE(ei.has_value()); + ASSERT_FALSE(ev.has_value()); + EXPECT_THROW(std::as_const(ei).value(), utils::bad_expected_access); EXPECT_THROW(ei.value(), utils::bad_expected_access); EXPECT_THROW(std::move(ei).value(), utils::bad_expected_access); EXPECT_THROW(ev.value(), utils::bad_expected_access); } -TEST(Expected, ErrorThrowsIfExpectedContainsValue) { +TEST(Expected, ErrorThrowsIfExpectedContainsNoError) { ExpectedInt ei{10}; ExpectedVoid ev; - EXPECT_THROW(const_cast(ei).error(), utils::bad_expected_access); + ASSERT_FALSE(ei.has_error()); + ASSERT_FALSE(ev.has_error()); + EXPECT_THROW(std::as_const(ei).error(), utils::bad_expected_access); EXPECT_THROW(ei.error(), utils::bad_expected_access); - EXPECT_THROW(const_cast(ev).error(), utils::bad_expected_access); + EXPECT_THROW(std::as_const(ev).error(), utils::bad_expected_access); EXPECT_THROW(ev.error(), utils::bad_expected_access); } +TEST(Expected, ValuelessByException) { + struct Throw { + Throw() = default; + Throw(const Throw&) { throw 0; } + Throw& operator=(const Throw&) { throw 0; } + }; + using Expected = utils::expected; + + Expected e(utils::unexpected(0)); + try { + e = Expected{}; + FAIL(); + } catch (...) {} + + EXPECT_FALSE(e.has_value()); + EXPECT_THROW(e.value(), utils::bad_expected_access); + EXPECT_FALSE(e.has_error()); + EXPECT_THROW(e.error(), utils::bad_expected_access); +} + USERVER_NAMESPACE_END