diff --git a/docs/message.adoc b/docs/message.adoc index 3a5d818b..265aa5a0 100644 --- a/docs/message.adoc +++ b/docs/message.adoc @@ -192,6 +192,22 @@ auto data = msg.data(); This always returns a (const-observing) `stdx::span` over the underlying data. +=== Message equivalence + +Equality (`operator==`) is not defined on messages. A general definition of +equality is problematic, but that doesn't mean we can't have a useful notion of +equivalence that is spelled differently: + +[source,cpp] +---- +auto m1 = my_message{"my_field"_field = 42}; +auto m2 = my_message{"my_field"_field = 0x2a}; +assert(equivalent(m1.as_const_view(), m2.as_mutable_view())); +---- + +Equivalence means that all fields hold the same values. It is defined for all +combinations of owning messages, const views and mutable views. + === Handling messages with callbacks _cib_ contains an implementation of a basic message handler which can be used in diff --git a/include/msg/message.hpp b/include/msg/message.hpp index 5d6db181..fe670ad0 100644 --- a/include/msg/message.hpp +++ b/include/msg/message.hpp @@ -397,11 +397,40 @@ template struct message { [[nodiscard]] constexpr auto data() const { return storage; } [[nodiscard]] constexpr auto as_owning() { return owner_t{*this}; } + [[nodiscard]] constexpr auto as_const_view() const { + using cv_t = + view_t, + stdx::ct_capacity_v>>; + return cv_t{*this}; + } private: static_assert(definition_t::fits_inside, "Fields overflow message storage!"); span_t storage{}; + + friend constexpr auto operator==(view_t, view_t) -> bool { + static_assert(stdx::always_false_v, + "Equality is not defined for messages: " + "consider using equivalent() instead."); + return false; + } + + using const_span_t = + stdx::span, + stdx::ct_capacity_v>; + using mutable_span_t = + stdx::span>; + + friend constexpr auto equiv(view_t lhs, + view_t rhs) -> bool { + return (... and (lhs.get(Fields{}) == rhs.get(Fields{}))); + } + + friend constexpr auto equiv(view_t lhs, + view_t rhs) -> bool { + return equiv(lhs, rhs.as_const_view()); + } }; using const_view_t = view_t; using mutable_view_t = view_t; @@ -478,9 +507,27 @@ template struct message { "Fields overflow message storage!"); storage_t storage{}; - friend constexpr auto operator==(owner_t const &lhs, - owner_t const &rhs) -> bool { - return lhs.storage == rhs.storage; + friend constexpr auto operator==(owner_t const &, + owner_t const &) -> bool { + static_assert(stdx::always_false_v, + "Equality is not defined for messages: " + "consider using equivalent() instead."); + return false; + } + + using const_span_t = stdx::span>; + using mutable_span_t = stdx::span>; + + friend constexpr auto equiv(owner_t const &lhs, + view_t rhs) -> bool { + return (... and (lhs.get(Fields{}) == rhs.get(Fields{}))); + } + + friend constexpr auto equiv(owner_t const &lhs, + view_t rhs) -> bool { + return equiv(lhs, rhs.as_const_view()); } }; @@ -518,8 +565,8 @@ template struct message { using matcher_t = decltype(match::all(typename Fields::matcher_t{}...)); - // NewFields go first because they override existing fields of the same name - // (see unique_by_name) + // NewFields go first because they override existing fields of the same + // name (see unique_by_name) template using extension = message_without_unique_field_names; @@ -534,6 +581,16 @@ template using owning = typename T::template owner_t<>; template using mutable_view = typename T::mutable_view_t; template using const_view = typename T::const_view_t; +template +concept messagelike = + requires { typename std::remove_cvref_t::definition_t; }; +template +concept viewlike = + messagelike and requires { typename std::remove_cvref_t::span_t; }; +template +concept owninglike = + messagelike and requires { typename std::remove_cvref_t::storage_t; }; + template concept view_of = std::same_as and Def::template fits_inside; @@ -543,4 +600,26 @@ concept const_view_of = template using extend = typename Def::template extension; + +template ::definition_t> V> +constexpr auto equivalent(O &&o, V v) -> bool { + return equiv(std::forward(o), v); +} + +template ::definition_t> V> +constexpr auto equivalent(V v, O &&o) -> bool { + return equiv(std::forward(o), v); +} + +template V2> +constexpr auto equivalent(V1 v1, V2 v2) -> bool { + return equiv(v1, v2); +} + +template +constexpr auto equivalent(O1 const &lhs, O2 const &rhs) { + return equiv(lhs, rhs.as_const_view()); +} } // namespace msg diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 522ccf17..8263d957 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -131,6 +131,8 @@ add_compile_fail_test(msg/fail/owning_msg_incompatible_storage.cpp LIBRARIES warnings cib) add_compile_fail_test(msg/fail/owning_msg_incompatible_view.cpp LIBRARIES warnings cib) +add_compile_fail_test(msg/fail/message_cmp_owner.cpp LIBRARIES warnings cib) +add_compile_fail_test(msg/fail/message_cmp_view.cpp LIBRARIES warnings cib) add_compile_fail_test(msg/fail/message_const_field_write.cpp LIBRARIES warnings cib) add_compile_fail_test(msg/fail/message_dangling_view.cpp LIBRARIES warnings cib) diff --git a/test/msg/fail/message_cmp_owner.cpp b/test/msg/fail/message_cmp_owner.cpp new file mode 100644 index 00000000..076e4f91 --- /dev/null +++ b/test/msg/fail/message_cmp_owner.cpp @@ -0,0 +1,19 @@ +#include +#include + +// EXPECT: consider using equivalent +namespace { +using namespace msg; + +using test_field1 = + field<"f1", std::uint32_t>::located; +using test_field2 = field<"f2", std::uint32_t>::located; + +using msg_defn = message<"test_msg", test_field1, test_field2>; +} // namespace + +auto main() -> int { + owning m1{}; + auto m2 = m1; + [[maybe_unused]] auto b = m1 == m2; +} diff --git a/test/msg/fail/message_cmp_view.cpp b/test/msg/fail/message_cmp_view.cpp new file mode 100644 index 00000000..971e902c --- /dev/null +++ b/test/msg/fail/message_cmp_view.cpp @@ -0,0 +1,20 @@ +#include +#include + +// EXPECT: consider using equivalent +namespace { +using namespace msg; + +using test_field1 = + field<"f1", std::uint32_t>::located; +using test_field2 = field<"f2", std::uint32_t>::located; + +using msg_defn = message<"test_msg", test_field1, test_field2>; +} // namespace + +auto main() -> int { + owning m{}; + auto mv1 = m.as_mutable_view(); + auto mv2 = m.as_mutable_view(); + [[maybe_unused]] auto b = mv1 == mv2; +} diff --git a/test/msg/message.cpp b/test/msg/message.cpp index 624ae0c1..ca7f4496 100644 --- a/test/msg/message.cpp +++ b/test/msg/message.cpp @@ -3,6 +3,14 @@ #include +#include +#include +#include +#include +#include +#include +#include + namespace { using namespace msg; @@ -212,7 +220,18 @@ TEST_CASE("owning from view", "[message]") { auto v = msg.as_mutable_view(); auto o = v.as_owning(); static_assert(std::is_same_v); - CHECK(msg == o); + CHECK(std::equal(msg.data().begin(), msg.data().end(), o.data().begin())); +} + +TEST_CASE("const view from mutable view", "[message]") { + test_msg msg{}; + auto mv = msg.as_mutable_view(); + auto v = mv.as_const_view(); + static_assert( + std::is_same_v); + msg.set("f1"_field = 0xba11); + CHECK(0xba11 == v.get("f1"_field)); } TEST_CASE("equal_to matcher", "[message]") { @@ -431,3 +450,53 @@ TEST_CASE("extend message with new field constraint", "[message]") { using expected_defn = message<"msg", id_field, field1::with_required<1>>; static_assert(std::is_same_v); } + +TEST_CASE("message equivalence (owning)", "[message]") { + test_msg m1{"f1"_field = 0xba11, "f2"_field = 0x42, "f3"_field = 0xd00d}; + test_msg m2{"f1"_field = 0xba11, "f2"_field = 0x42, "f3"_field = 0xd00d}; + CHECK(equivalent(m1, m2)); + test_msg other{"f1"_field = 0xba11, "f2"_field = 0x42, "f3"_field = 0xd00f}; + CHECK(not equivalent(m1, other)); +} + +TEST_CASE("message equivalence (owning/const_view)", "[message]") { + owning m{"f1"_field = 0xba11, "f2"_field = 0x42, + "f3"_field = 0xd00d}; + auto cv = m.as_const_view(); + CHECK(equivalent(m, cv)); + CHECK(equivalent(cv, m)); + + owning other{"f1"_field = 0xba11, "f2"_field = 0x42, + "f3"_field = 0xd00f}; + CHECK(not equivalent(other, cv)); + CHECK(not equivalent(cv, other)); +} + +TEST_CASE("message equivalence (owning/mutable_view)", "[message]") { + owning m{"f1"_field = 0xba11, "f2"_field = 0x42, + "f3"_field = 0xd00d}; + auto mv = m.as_mutable_view(); + CHECK(equivalent(m, mv)); + CHECK(equivalent(mv, m)); + + owning other{"f1"_field = 0xba11, "f2"_field = 0x42, + "f3"_field = 0xd00f}; + CHECK(not equivalent(other, mv)); + CHECK(not equivalent(mv, other)); +} + +TEST_CASE("message equivalence (views)", "[message]") { + owning m{"f1"_field = 0xba11, "f2"_field = 0x42, + "f3"_field = 0xd00d}; + auto cv1 = m.as_const_view(); + auto mv1 = m.as_mutable_view(); + CHECK(equivalent(cv1, cv1)); + CHECK(equivalent(mv1, mv1)); + CHECK(equivalent(cv1, mv1)); + CHECK(equivalent(mv1, cv1)); + + owning other{"f1"_field = 0xba11, "f2"_field = 0x42, + "f3"_field = 0xd00f}; + auto cv2 = other.as_const_view(); + CHECK(not equivalent(cv1, cv2)); +}