-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use <=> operator for base_uint, Issue, and Book: #4411
Use <=> operator for base_uint, Issue, and Book: #4411
Conversation
It seems that std::lexicographical_compare_three_way is not yet supported by Xcode. I'll look into alternatives. |
PR is ready for review. It would be nice to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. I think the code can be further improved and cleaned up. I left some comments.
src/ripple/basics/base_uint.h
Outdated
template <std::size_t Bits, typename Tag> | ||
class base_uint; | ||
|
||
template <std::size_t Bits, typename Tag> | ||
[[nodiscard]] constexpr bool | ||
operator==(base_uint<Bits, Tag> const& lhs, base_uint<Bits, Tag> const& rhs); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All this is unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention was to be able to compare the arrays data_
with std::uint32_t
data elements instead of using std::mismatch()
with unsigned char
data elements. It's probably an overkill. I revert this change and take your suggested code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see what you were trying to do. It makes sense, but you need to be careful (and you were). This trick will ONLY work for equality/inequality comparisons, because the data_
array is stored in big endian order, which on platforms like the x86 is NOT the native endian order. Relational comparisons (gt/gte/lt/lte) break. Consider, for example, the following two 32-bit unsigned values:
a = 256, b = 1
If you convert them into big endian they look like
x = 65536, y = 16777216
Notice what happened... a > b
but x < y
. Oops!
I think that having the "bespoke" operator==
is fine although I doubt there are any performance gains to be had. But you should definitely consider adding a comment explaining that this is safe, despite the fact that the individual limbs (the entries in the data_
array) are in big endian, because we are comparing for equality/inequality.
The existing compare
function CANNOT do that trick and MUST use std::mismatch
. It could also use a comment too, explaining that this works because the pointer returned from cbegin
is a byte-based view of the data_
which is actually in big-endian order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. This is quite tricky. I use std::mismatch
for now.
src/ripple/protocol/Book.h
Outdated
[[nodiscard]] bool | ||
operator==(Book const& lhs, Book const& rhs); | ||
bool | ||
operator!=(Book const& lhs, Book const& rhs); | ||
/** @} */ | ||
|
||
/** Strict weak ordering. */ | ||
/** @{ */ | ||
bool | ||
operator<(Book const& lhs, Book const& rhs); | ||
bool | ||
operator>(Book const& lhs, Book const& rhs); | ||
bool | ||
operator>=(Book const& lhs, Book const& rhs); | ||
bool | ||
operator<=(Book const& lhs, Book const& rhs); | ||
[[nodiscard]] std::weak_ordering | ||
operator<=>(Book const& lhs, Book const& rhs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just use defaulted comparisons for these. Simply add the following inside the declaration of Book
as a public
and you automatically get all six relational operators:
auto operator<=>(Bool const&) const = default;
NOTE, HOWEVER, THAT THIS HAS A RISK:* first, the order of the member variables matters (since the default comparison goes through each member in order) and if any additional variables are added, it might cause subtle breakage of comparisons.
With that in mind, I'm not sure it's worth defaulting, but just pointing it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. We could default here but the operator<=>
becomes a bit fragile as you pointed out.
I would not default unless you insist. Please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Left some small comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! As far as I can see everything should work correctly.
I left a number of comments for things I think might be minor improvements. Specifically, I think you're working too hard when you use the stored result of the <=>
operator. You don't need to use constants like std::weak_ordering::equivalent
. It is sufficient (and easier to read) to compare to literal 0
.
Comparison to 0
is how the compiler rewrites comparison expressions to use the <=>
operator. See section 2.2.3 of the original spaceship paper: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r3.pdf
I think that's the most important change to consider. I'm also open to being told I'm wrong. But I think it's useful to get this right early on, since this is the first introduction of the <=>
operator to this code base. People will be copying the example that you set here.
// FIXME: use std::is_eq and std::is_gt once support is | ||
// added to MacOS. | ||
auto const relation{(id & nft::pageMask) <=> cmp}; | ||
if (relation == std::strong_ordering::equivalent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works. So no change necessary. But I think you're working harder than you need to. Consider the rewrite rules that C++ follows with the <=>
operator:
(a <=> b) @ 0
So I don't think we need std::strong_ordering::equivalent
or std::is_eq
or std::is_gt
. All we need is 0
. I suggest rewriting the comparison like this:
auto const relation{(id & nft::pageMask) <=> cmp};
if (relation == 0)
I think that allows you to remove the FIXME. I think it's also easier to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this suggestion. It makes the code clean and concise.
|
||
else if (relation > 0) | ||
if (relation == std::strong_ordering::greater) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the above comment, you can simplify this to
if (relation > 0)
with no loss of generality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
// FIXME: use std::is_eq and std::is_lt once support is | ||
// added to MacOS. | ||
if (auto const c{(a & nft::pageMask) <=> (b & nft::pageMask)}; | ||
c != std::strong_ordering::equivalent) | ||
return c == std::strong_ordering::less; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this works, but I think you're working harder than necessary. A more minimal change might look like this:
if (auto const lowBitsCmp = (a & nft::pageMask) <=> (b & nft::pageMask);
lowBitsCmp != 0)
return lowBitsCmp < 0;
No FIXME comment necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
@@ -549,103 +550,77 @@ using uint160 = base_uint<160>; | |||
using uint256 = base_uint<256>; | |||
|
|||
template <std::size_t Bits, class Tag> | |||
inline int | |||
compare(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b) | |||
[[nodiscard]] inline constexpr std::strong_ordering |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the [[nodiscard]]
and constexpr
. Nice!
src/ripple/basics/base_uint.h
Outdated
// a > b | ||
if (*ret.first > *ret.second) | ||
return 1; | ||
return std::strong_ordering::greater; | ||
|
||
// a < b | ||
return -1; | ||
} | ||
|
||
template <std::size_t Bits, class Tag> | ||
inline bool | ||
operator<(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b) | ||
{ | ||
return compare(a, b) < 0; | ||
} | ||
|
||
template <std::size_t Bits, class Tag> | ||
inline bool | ||
operator<=(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b) | ||
{ | ||
return compare(a, b) <= 0; | ||
} | ||
|
||
template <std::size_t Bits, class Tag> | ||
inline bool | ||
operator>(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b) | ||
{ | ||
return compare(a, b) > 0; | ||
} | ||
|
||
template <std::size_t Bits, class Tag> | ||
inline bool | ||
operator>=(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b) | ||
{ | ||
return compare(a, b) >= 0; | ||
} | ||
|
||
template <std::size_t Bits, class Tag> | ||
inline bool | ||
operator==(base_uint<Bits, Tag> const& a, base_uint<Bits, Tag> const& b) | ||
{ | ||
return compare(a, b) == 0; | ||
return std::strong_ordering::less; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works fine. If you want you could reduce it a bit like this:
return (*ret.first > *ret.second) ? std::strong_ordering::greater
: std::strong_ordering::less;
That reduces from two return
s to one. But your choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait what? You can’t reduce lines 571-575 down to a single return.
Am I not understanding the suggestion here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is now like below.
auto const ret = std::mismatch(lhs.cbegin(), lhs.cend(), rhs.cbegin());
// a == b
if (ret.first == lhs.cend())
return std::strong_ordering::equivalent;
return (*ret.first > *ret.second) ? std::strong_ordering::greater
: std::strong_ordering::less;
We can use a ternary operator because we know that a != b
. If you want to revert this change, let me know.
src/test/basics/base_uint_test.cpp
Outdated
{"123456789abc", "aaaaaaaaaaaa"}, | ||
{"999999999999", "aaaaaaaaaaaa"}}; | ||
|
||
for (auto& arg : test_args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be auto const& arg
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
src/test/basics/base_uint_test.cpp
Outdated
|
||
for (auto& arg : test_args) | ||
{ | ||
ripple::base_uint<96> u{arg.first}, v{arg.second}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be
ripple::base_uint<96> const u{arg.first}, v{arg.second};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
src/test/basics/base_uint_test.cpp
Outdated
std::vector<std::pair<std::string, std::string>> test_args{ | ||
{"0123456789ab", "123456789abc"}, | ||
{"010101010101", "111111111111"}, | ||
{"000000000000", "010101010101"}, | ||
{"123456789abc", "aaaaaaaaaaaa"}, | ||
{"999999999999", "aaaaaaaaaaaa"}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, we're only exercising half the bits in the uint_base<96>
. Consider this alternative:
static constexpr std::array<
std::pair<std::string_view, std::string_view>,
6>
test_args{{
{"000000000000000000000000", "000000000000000000000001"},
{"000000000000000000000000", "ffffffffffffffffffffffff"},
{"0123456789ab0123456789ab", "123456789abc123456789abc"},
{"555555555555555555555555", "55555555555a555555555555"},
{"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"},
{"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"},
}};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
// This comparison might seem wrong on a casual inspection because it | ||
// compares data internally stored as std::uint32_t byte-by-byte. But | ||
// note that the underlying data is stored in big endian, even if the | ||
// plaform is little endian. This makes the comparison correct. | ||
// | ||
// FIXME: use std::lexicographical_compare_three_way once support is | ||
// added to MacOS. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice comment!
src/test/basics/base_uint_test.cpp
Outdated
static_assert(std::is_copy_constructible<test96>::value, ""); | ||
static_assert(std::is_copy_assignable<test96>::value, ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are pre-C++17 static_assert
s. While you're in the file would you consider removing the closing , ""
? Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! I spotted one more possible change, but it's not critical. So I'm approving. Feel free to make that change if you want, even with this approval.
When you squash all of these commits, try to keep the first line of the commit message under 60 characters or so. You might consider something like this for your commit message:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Looks great! Nice work.
e873b55
to
f7daa6d
Compare
@drlongle if you feel this is ready to merge, you can put the |
* upstream/develop: Rectify the import paths of boost/iterator: (XRPLF#4293) Allow port numbers be be specified with a colon: (XRPLF#4328) Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411) Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
* upstream/develop: Rectify the import paths of boost/iterator: (XRPLF#4293) Allow port numbers be be specified with a colon: (XRPLF#4328) Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411) Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
* upstream/develop: Rectify the import paths of boost/iterator: (XRPLF#4293) Allow port numbers be be specified with a colon: (XRPLF#4328) Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411) Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
* upstream/develop: Rectify the import paths of boost/iterator: (XRPLF#4293) Allow port numbers be be specified with a colon: (XRPLF#4328) Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411) Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
* upstream/develop: Rectify the import paths of boost/iterator: (XRPLF#4293) Allow port numbers be be specified with a colon: (XRPLF#4328) Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411) Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
…ctionality * upstream/develop: Rectify the import paths of boost/iterator: (XRPLF#4293) Allow port numbers be be specified with a colon: (XRPLF#4328) Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411) Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
…tpage * upstream/develop: Rectify the import paths of boost/iterator: (XRPLF#4293) Allow port numbers be be specified with a colon: (XRPLF#4328) Use <=> operator for base_uint, Issue, and Book: (XRPLF#4411) Reporting Mode: Do not attempt to acquire missing data from peer network (XRPLF#4458)
High Level Overview of Change
This PR resolves issue 2525. The PR implements the
operator==
and theoperator<=>
(aka the spaceship operator) inbase_uint
,Issue
, andBook
. C++20-compliant compilers automatically provide the remaining comparison operators (e.g.operator<
,operator<=
, ...). I also remove the functioncompare()
because it is no longer needed. The PR provides the same semantics as the existing code. All unit tests pass. I also added some unit tests to gain further confidence.Type of Change