-
Notifications
You must be signed in to change notification settings - Fork 80
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
feat: zero
support added
#488
Conversation
Resolves #487
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.
LGTM, thanks!
In my experience, the biggest use case for zero
is comparison and sign checking. Initialization can also be really handy.
BTW, we later added an explicit = delete;
for the zero
constructor of quantity_point
. I think that'd be nice here too.
I don't think it is needed as in mp-units we do not have a constructor in |
My main motivation for adding it was to communicate clearly to users that yes, we thought about this and no, we don't want to support it. I was anticipating that users who get "spoiled" by using I guess I'm trying to head off a misguided feature request before it can happen. 🙂 |
It's more readable if it's a line comment after
|
I think I would add it as well if we had a constructor from |
Unfortunately, we have to get rid of some interfaces for |
Hmm, that's too bad. Then again, it's probably just fine to start small and deliver value in a few use cases, then expand later as we're able. By the way, there's one more non-obvious use case for |
I do not think it will be possible. For example the below would compile: QuantityOf<isq::energy> auto q = isq::width(2 * m) * (mean_sea_level - mean_sea_level); which would be really problematic, right? It is exactly why I removed |
If we made the result of subtracting an origin from itself |
Yes, you are right, but I think making it not compile would be wrong and inconsistent. Why the first one should not compile while others compile fine: auto q1 = isq::width(2 * m) * (mean_sea_level - mean_sea_level);
auto q2 = isq::width(2 * m) * (ground_level - mean_sea_level);
auto q3 = isq::width(2 * m) * (my_quantity_point - mean_sea_level);
auto q3 = isq::width(2 * m) * (my_quantity_point1 - my_quantity_point2); Fixed the code after the comment below What if I have a generic function like this: constexpr auto point_diff(const QuantityPointOrPointOrigin& lhs, const QuantityPointOrPointOrigin& rhs)
{
return lhs - rhs;
} or something similar. How do we explain to the user that sometimes a result from such a function would make the following code not compile? |
To summarize, I think that if we decide that the subtraction of two points (that typically returns a |
… about `zero` self-assignment
Ah --- so in #488 (comment), you meant to say that it would not compile, and that this is what would be problematic. Now I understand the argument. It's a fair point. (Also, I think you're assigning an area to an energy in all of these examples, but that's a side point. 🙂) This is an analogous problem to the decision to return a raw number when units cancel completely. I think mp-units used to do that, but then stopped. Au did not use to do that, and we now do --- here's the rationale --- but I'm starting to second-guess that decision. If I could go back in time, I'd probably advise my past self to prefer consistency and just always return a quantity. So I'm persuaded to avoid returning |
friend consteval zero_t operator%(zero_t, zero_t) = delete; | ||
|
||
[[nodiscard]] consteval bool operator==(zero_t) const { return true; } | ||
[[nodiscard]] consteval auto operator<=>(zero_t) const { return std::partial_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.
Why not 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.
Initially, I thought that the following is the reason for this:
static_assert(zero == zero);
static_assert(!(zero != zero));
static_assert(!(zero < zero));
static_assert(!(zero > zero));
but you are right. I see now that I was wrong. Thanks!
Fixed the code. Thanks for pointing this out. |
I actually meant both. There is no good solution for that. Either you make it not compile which makes it inconsistent or you make it compile and suddenly |
Besides the current |
I think that would be an overkill of a feature. In order to use it must be simple and universal. It should be usable not only with physical units but also with other numeric libraries. Today, I sent a question to the SG6 (Numerics) chair, asking if he finds it useful in other use cases. If he is not interested, I am not sure if we will merge it as I see too many issues with this solution compared to the benefits of having it. |
I agree: dimension-based I'm surprised we might not merge it if the SG6 chair doesn't already know of other use cases. I think the units library use cases are enough to justify it. It's true that we expect For the units library use cases specifically, I think it makes sense to start conservatively with "whatever we can get by making I also think it makes sense to think of Ultimately, though, the decision to include or exclude |
I am really puzzled here. It is much easier to add it later (especially as it is already implemented on the branch) when we find it mandatory than remove it later when people will actually start using it, but when we decide it was not a good idea. I see some use cases where it helps, but I also see some cases where it actually causes lots of confusion. Even such an experienced user with this feature as you, suggested using it for the subtraction of two points because it really feels like it is the right way to do it. But it is not... And there are more possible confusions coming. I am afraid of GitHub issues asking why the following does not work: msl_altitude current_ = mean_sea_level + zero or this one: for (auto tt = zero; tt <= 50 * ms; ++tt) {
// ...
} or this: auto q1 = 42 * m;
auto q2 = q1 * zero; All of the above looks like the code that "should" work with a feature
Again, despite your long experience with the feature (please don't treat it personally; I just want to make a point here that even experts might be easily confused here), even the above use case has some issues connected with it: void foo(quantity<si::metre> q);
void boo(QuantityOf<isq::length> auto q);
foo(zero); // works
boo(zero); // does not work And what if someone will be already using Another similar issue with the above-proposed design: auto q1 = 42 * m;
q1 += zero; // works
auto q2 = q1 + zero; // does not work Initially, I thought that As I said, there are certainly some cases where it makes the code nicer or more generic, but we may fall into plenty of possible misuses and pitfalls if we add it. Let's remember that this feature is mostly to write this: if (v1 - v2 == zero) ... instead of this: if (v1 - v2 == 0 * (m/s)) ... I am not so convinced that it is worth it... |
tl;dr: Thanks for collecting these problem cases in one place! I looked at each, and I didn't find most of them compelling, because I don't believe users would be motivated to write them in the first place. However, I do think the issue of passing
That's true. It's always easier to add later than to remove.
Which comment are you referring to? Was it this one or some other? Assuming that's the one, I think it was an oversight on my part that I didn't question the premise of your comment: namely, that we would want What the rest of that comment refers to is an encapsulated, library-internal implementation detail. There's a use case when computing origin offsets where we want to use
Thanks for supplying all of these concrete examples in one place! I'll look at each one in turn, to see how much it motivates me to avoid or defer adding
I'm not fluent with mp-units constructs, so I'll write out my assumptions explicitly. I assume:
If those assumptions are correct, then My question: would we expect this to work without the
Here's another use case where I just don't think users are motivated to reach for I think your main point, though, is that if a user does try this, then we might get an annoying bug report. I don't think we need to worry about this. The compiler error will mention In fact, I think the situation is analogous to something we already have with an existing standard type that represents a kind of "zero-ness": auto foo_ptr = nullptr;
++foo_ptr; Next issue:
I concede that if It's interesting to contrast the additive and multiplicative properties for
So, yes:
Thanks for clarifying that it's not meant personally. 🙂 And don't worry, I haven't taken anything personally, and I understand the point you're making by referencing my experience. In fact, I think the below use case is really interesting.
I think the reason the "implicitly convertible" use cases are such a slam dunk for Au, but have questionable use cases like this for mp-units, is at least in part due to concepts. Au virtually always deals in concrete types --- and as long as I don't have a fully satisfying general answer. In this specific case, if one refactored an interface like this, and if there are many existing callsites using The big takeaway, though, is that "
I'm confused: why wouldn't this work? I assume TEST(Zero, CanAddToQuantity) {
auto q1 = meters(42);
auto q2 = q1 + ZERO;
EXPECT_THAT(q2, SameTypeAndValue(q1));
} The test passes. That said, even if it didn't, why would users be motivated to add zero to the quantity?
I'm persuaded by the argument that it's easier to add than to remove, so if you're feeling uncomfortable about it, then I support your decision to defer.
Well, what are bool equal(QuantityOf<isq::speed> auto v1, QuantityOf<isq::speed> auto v2) {
return (v1 - v2 == 0 * (m/s));
} In this case, we may get an extra unit conversion at runtime, between It's also really interesting that in this case, concepts actually strengthen the arguments for |
Thanks for the detailed comments and answers! Here are some comments from me:
Yes, this is the one I meant.
Everything depends on the use case. Let's imagine this: msl_altitude make_AMSL(auto q)
{
return mean_sea_level + q;
}
msl_altitude qp1 = make_AMSL(42 * m); // works
msl_altitude qp1 = make_AMSL(zero); // error
I also think that the user will understand the compiler error but we will get a bug report anyway asking us to make it work 😉 And we will have to explain (possibly multiple times to multiple users) why it can't work and why we won't "fix" it.
It is not only about concepts but also about plain
Through the years of doing mp-units, I have learned that mp-units/src/core/include/mp-units/quantity.h Lines 340 to 346 in d7261c8
The above does not allow any implicit conversions. However, the compound assignment is different. It can't change the LHS type, and in the case of self-addition it means that the RHS must be of the same type. This means that we can allow implicit conversions here: mp-units/src/core/include/mp-units/quantity.h Lines 236 to 245 in d7261c8
This difference was not a big issue for now, but adding implicit conversion from
The
This is true, and I see this as the biggest benefit of this feature. Otherwise, we need something like this: bool foo(QuantityOf<isq::speed> auto v1, QuantityOf<isq::speed> auto v2)
{
const auto q = v1 - v2;
return (q == q.zero());
} to prevent additional conversions. This is a bit inconvenient but not that terrible IMHO. As I wrote before, I really liked the idea of I see the benefits of this feature but I just don't know for now how to make it work in an easy-to-understand way. In this commit 0dd28c1 I already removed subtraction of points, and addition and subtraction of a
Is it enough to justify the addition of a new feature? Please let me know your thoughts. |
Interesting... I see that this is implemented as a nonmember function. Au takes a different approach. We haven't seen ODR violations or lack of commutativity, but maybe we have those problems and just haven't encountered them! Our main approach to implementing quantity/quantity addition is twofold. First, we use the hidden friend idiom for same-quantity-type addition: Next, we use a nonmember function for adding two different quantity types: (There are two other overloads, but they cover the cases for corresponding quantities --- so, adding an Au quantity with one from nholthaus or the chrono library, say. I'm glossing over them for now.) This setup has worked well so far. It preserves commutativity. It also allows us to use one-way implicitly convertible types such as
On reflection, I think implicit conversion is really important to get good usability. I still feel |
I tried making them hidden friends a few times already, but I always failed every single time because of some corner and surprising cases connected to implicit conversions of quantities. |
I thought a lot about I think we have two options here:
|
Resolves #487