mp-units icon indicating copy to clipboard operation
mp-units copied to clipboard

Should `math.h` be implemented as hidden friends?

Open mpusz opened this issue 10 months ago • 11 comments

Should we implement the functions from the mp_units/math.h header as non-member hidden friend functions in quantity and quantity_point?

mpusz avatar Jan 24 '25 13:01 mpusz

Maybe!

The main benefit is that it would let us automatically support types that implicitly convert to, but not from, a quantity or quantity_point. (That said, mp-units has traditionally aligned itself against using this sort of type: zero was rejected, and mp-units constants do not convert to quantities.)

Another benefit is reducing the size of overload sets, which can make for shorter compiler error messages and, I think, faster compile times in a few cases.

The downside is that it's an intrusive change: we need to open up the guts of quantity and quantity_point to add it. Also, it locks us in to forcing a <cmath> dependency on all users who include these files. (Maybe this is already the case; I didn't check.)

Au has done this for min, max, clamp, and %, without the <cmath> dependency. We get a lot more benefit out of it than mp-units would, because of our "shapeshifter types" (i.e., Zero and Constant<U>).

Later on, I tried using this for remainder (which wraps std::remainder), passing a Constant as one of the arguments, and it failed. I realized that in order to get this to work, we'd need to take this hidden friend approach for remainder. It felt like opening up a bit of a Pandora's box, so I didn't add it at the time, and still haven't added it. I'm not sure yet how to decide which ones should or shouldn't get this treatment.

chiphogg avatar Jan 25 '25 19:01 chiphogg

To be concrete: which specific functions were you considering?

chiphogg avatar Jan 25 '25 19:01 chiphogg

I thought about most if not all, functions from the mp_units/math.h header. Additionally, things like lerp and midpoint should be provided for quantity_point.

mpusz avatar Jan 26 '25 10:01 mpusz

and mp-units constants do not convert to quantities

Are constants in Au modeled as units that convert to quantities?

mpusz avatar Jan 26 '25 10:01 mpusz

and mp-units constants do not convert to quantities

Are constants in Au modeled as units that convert to quantities?

No, they are constants that convert to quantities. 🙂 Constant<U> is a template that is templated on the unit U, and has a perfect conversion policy (no heuristic overflow safety surface needed) with to any Quantity<OtherU, R>: every lossy conversion fails, and every lossless conversion succeeds.

chiphogg avatar Jan 26 '25 18:01 chiphogg

But I assume that such a constant is a part of the unit of a quantity and simplifies at compile-time when needed?

mpusz avatar Jan 27 '25 07:01 mpusz

But I assume that such a constant is a part of the unit of a quantity and simplifies at compile-time when needed?

I don't understand what you're asking, but I understand what Au does. 🙂 Every Constant<U> is templated only on the unit U; therefore, we need to have (or create) a unit for each constant. Constant<U> is a monovalue type, so this unit basically is its value: its value is "1 U", so to speak. If you try to assign this constant to a Quantity<U1, R1>, this assignment will succeed if 1 U can be exactly represented in Quantity<U1, R1>, and fail otherwise. And if it succeeds, the value in U1 and R1 will be computed at compile time.

The parts I didn't understand or found confusing were:

  • "such a constant is a part of the unit of a quantity"
  • "simplifies at compile-time when needed"

I hope my above paragraph answered the question.

chiphogg avatar Jan 27 '25 14:01 chiphogg

I mean something like this:

constexpr auto c = speed_of_light;
quantity q1 = 0.4 * c;
quantity q2 = q1 / c;
  1. What is the unit of q1 (is it c or some other arbitrary unit of speed together with a proper compile-time magnitude)?
  2. I assume that q1 stores just 0.4, and no calculations are done at this point.
  3. I assume that q2 is a dimensionless quantity with exactly a value 0.4, and there was no runtime cost of the division in the initializer.

mpusz avatar Jan 27 '25 16:01 mpusz

Thanks for giving a concrete example. I think the answers will be just as you expect (with the caveat that this syntax wouldn't work for Au because we don't have deduction guides for C++17's CTAD):

  1. The units of q1 are the units of c.
  2. Yep, we store just 0.4 under the hood, with no calculations.
  3. Yep, q2 would be just a dimensionless quantity with exactly 0.4, and no runtime cost.

I'm a little uncertain for (3) as to whether we would produce a quantity or just a raw double, but if it's the latter then it's what we now consider a design defect and we plan to fix it in the next "breaking change" release (probably 0.5.0).

chiphogg avatar Jan 27 '25 16:01 chiphogg

Thanks. So, it seems the only drawback of this solution to what we have in mp-units is that a possibly long magnitude of a constant is visible in a quantity type. When two constants are involved, magnitude no longer has a "familiar" value. But you gained implicit constructions from a constant.

I also think that Au uses implicit conversions for operators. It means that things like c - 0.5 * c and -0.5 * c + c will probably work? In our case, where we do not have an implicit conversion and arguments are deduced function template parameters, we would need to add dedicated overloads for constants.

mpusz avatar Jan 28 '25 07:01 mpusz

I also think that Au uses implicit conversions for operators. It means that things like c - 0.5 * c and -0.5 * c + c will probably work?

Apparently so!

chiphogg avatar Jan 28 '25 13:01 chiphogg