From b2cf19ceadfc62bb33088b9e65f57836cb25334d Mon Sep 17 00:00:00 2001 From: X1aomu Date: Tue, 13 Oct 2026 09:41:30 +0800 Subject: [PATCH 1/2] fix: derive unexpected inequality from equality Use wrapper equality and its exception specification in C++17, and rely on rewritten comparisons in C++20 and later. This supports equality-only error types and propagates exceptions from equality or bool conversion. Wrapper inequality now follows the negation of wrapper equality even when the error type defines != independently. --- include/zeus/expected.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/zeus/expected.hpp b/include/zeus/expected.hpp index 0463be4..686037a 100644 --- a/include/zeus/expected.hpp +++ b/include/zeus/expected.hpp @@ -251,11 +251,13 @@ class unexpected ); return lhs.error() == rhs.error(); } +#if ZEUS_EXPECTED_CPLUSPLUS < 202'002L template - friend constexpr bool operator!=(const unexpected &lhs, const unexpected &rhs) noexcept(noexcept(lhs.error() != rhs.error())) + friend constexpr bool operator!=(const unexpected &lhs, const unexpected &rhs) noexcept(noexcept(lhs == rhs)) { - return lhs.error() != rhs.error(); + return !(lhs == rhs); } +#endif private: E m_val; From a3b6cb1a2fac01aea5beab2fd42c41f6dbaddefa Mon Sep 17 00:00:00 2001 From: X1aomu Date: Tue, 13 Oct 2026 10:32:08 +0800 Subject: [PATCH 2/2] test: cover unexpected inequality semantics and exceptions --- tests/test_expected/CMakeLists.txt | 1 + .../unexpected_equality_tests.cpp | 139 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/test_expected/unexpected_equality_tests.cpp diff --git a/tests/test_expected/CMakeLists.txt b/tests/test_expected/CMakeLists.txt index 4270866..c72a853 100644 --- a/tests/test_expected/CMakeLists.txt +++ b/tests/test_expected/CMakeLists.txt @@ -10,6 +10,7 @@ set(SOURCES swap_exception_tests.cpp equality_tests.cpp equality_noexcept_tests.cpp + unexpected_equality_tests.cpp p3379_tests.cpp lwg_3886_tests.cpp lwg_4031_tests.cpp diff --git a/tests/test_expected/unexpected_equality_tests.cpp b/tests/test_expected/unexpected_equality_tests.cpp new file mode 100644 index 0000000..81e77c0 --- /dev/null +++ b/tests/test_expected/unexpected_equality_tests.cpp @@ -0,0 +1,139 @@ +#include + +#include + +#include + +namespace +{ + +// Comparison results need only an implicit conversion to bool, not explicit conversion or negation. +struct ImplicitBoolean +{ + bool value; + + constexpr operator bool() const noexcept { return value; } + explicit operator bool() = delete; + bool operator!() const = delete; +}; + +template +struct EqualityOnlyError +{ + T value; +}; + +template +constexpr ImplicitBoolean operator==(const EqualityOnlyError &lhs, const EqualityOnlyError &rhs) noexcept +{ + return {lhs.value == rhs.value}; +} + +// Like Boost.Interval's "certain" comparisons, equality and inequality can both be false. +struct IntervalError +{ + int lower; + int upper; + + friend constexpr bool operator==(const IntervalError &lhs, const IntervalError &rhs) noexcept + { + return lhs.lower == lhs.upper && rhs.lower == rhs.upper && lhs.lower == rhs.lower; + } + + friend constexpr bool operator!=(const IntervalError &lhs, const IntervalError &rhs) noexcept(false) + { + return lhs.upper < rhs.lower || rhs.upper < lhs.lower; + } +}; + +struct ComparisonFailure +{ +}; + +struct ThrowingBoolean +{ + operator bool() const { throw ComparisonFailure {}; } +}; + +struct ConversionThrows +{ + friend ThrowingBoolean operator==(const ConversionThrows &, const ConversionThrows &) noexcept { return {}; } + friend ThrowingBoolean operator!=(const ConversionThrows &, const ConversionThrows &) noexcept { return {}; } +}; + +struct ComparisonThrows +{ + friend bool operator==(const ComparisonThrows &, const ComparisonThrows &) { throw ComparisonFailure {}; } + friend bool operator!=(const ComparisonThrows &, const ComparisonThrows &) noexcept { return false; } +}; + +} // namespace + +TEST_CASE("unexpected inequality supports equality-only errors with implicit bool results", "[unexpected][equality]") +{ + constexpr zeus::unexpected> lhs {EqualityOnlyError {42}}; + constexpr zeus::unexpected> equal {EqualityOnlyError {42}}; + constexpr zeus::unexpected> different {EqualityOnlyError {7}}; + constexpr zeus::unexpected> other_equal {EqualityOnlyError {42}}; + constexpr zeus::unexpected> other_different {EqualityOnlyError {7}}; + + STATIC_REQUIRE(noexcept(lhs != equal)); + STATIC_REQUIRE(noexcept(lhs != other_equal)); + STATIC_REQUIRE(noexcept(other_equal != lhs)); + + STATIC_REQUIRE(lhs == equal); + STATIC_REQUIRE_FALSE(lhs != equal); + STATIC_REQUIRE(lhs != different); + STATIC_REQUIRE_FALSE(lhs != other_equal); + STATIC_REQUIRE_FALSE(other_equal != lhs); + STATIC_REQUIRE(lhs != other_different); + STATIC_REQUIRE(other_different != lhs); + + CHECK(lhs == equal); + CHECK_FALSE(lhs != equal); + CHECK(lhs != different); + CHECK_FALSE(lhs != other_equal); + CHECK_FALSE(other_equal != lhs); + CHECK(lhs != other_different); + CHECK(other_different != lhs); +} + +TEST_CASE("unexpected inequality negates equality independently of error inequality", "[unexpected][equality][noexcept]") +{ + constexpr zeus::unexpected lhs { + IntervalError {1, 3} + }; + constexpr zeus::unexpected rhs { + IntervalError {2, 4} + }; + + STATIC_REQUIRE(noexcept(lhs != rhs)); + + STATIC_REQUIRE_FALSE(lhs.error() == rhs.error()); + STATIC_REQUIRE_FALSE(lhs.error() != rhs.error()); + STATIC_REQUIRE(lhs != rhs); + STATIC_REQUIRE(rhs != lhs); + + CHECK(lhs != rhs); + CHECK(rhs != lhs); +} + +TEST_CASE("unexpected comparisons propagate exceptions from bool conversion", "[unexpected][equality][noexcept]") +{ + const zeus::unexpected lhs {std::in_place}; + const zeus::unexpected rhs {std::in_place}; + + STATIC_REQUIRE_FALSE(noexcept(lhs == rhs)); + STATIC_REQUIRE_FALSE(noexcept(lhs != rhs)); + CHECK_THROWS_AS(lhs == rhs, ComparisonFailure); + CHECK_THROWS_AS(lhs != rhs, ComparisonFailure); +} + +TEST_CASE("unexpected inequality propagates exceptions from error equality", "[unexpected][equality][noexcept]") +{ + const zeus::unexpected lhs {std::in_place}; + const zeus::unexpected rhs {std::in_place}; + + STATIC_REQUIRE_FALSE(noexcept(lhs != rhs)); + CHECK_THROWS_AS(lhs != rhs, ComparisonFailure); +}