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

Make casts explicit in what they do, not what they apply to

Open burnpanck opened this issue 3 years ago • 67 comments

We currently have four different casts here: quantity_cast, quantity_point_cast, quantity_kind_cast and quantity_point_kind_cast. Those casts all happily perform any and all of the following tasks, depending on the argument types:

  1. Convert between different representations without changing the physical meaning, up to numerical issues due to quantisation and overflow. A change of representation may incur one or more of:
    • A change of the binary representation of the value, e.g. float to int
    • A change of the unit of measurement compensated by the corresponding scaling of the numerical value
    • (New, if my PR #232 makes it in) A change of the reference point when specifying physical points along a dimension with respect to some abstract, well-known origin. Example: Conversion between the representation of "room temperature" from 25 °C to 298.15 K.
  2. Re-identify physical values represented in one unit system of measurement into another unit system, by re-associating the same numerical value with a matching unit from that other system of measurement.
  3. Re-interpret physical measures as fundamentally different "kind of quantities" but with shared physical dimensions. E.g. reinterpret a torque (kind of quantity) of "100 N m" into an energy (a distinct kind of quantity) of "100 N m = 100 J", where that equals sign is one of symbolic but not necessarily semantic equivalency.
  4. (Edit: Wrong assessment of mine) Ignore the physical units of a measure and simply copy the numerical value in front of a different unit and thus into a different dimension.
  5. (Not even with PR #232) Re-interpret an absolute point at a certain distance to one origin, as a different absolute point at the same distance to a different origin.

These tasks have very different levels of physical justification behind them, and are in that sense very different in their destructiveness. If we compare that with the C++ standard, that feels a bit like all over static_cast, const_cast and reinterpret_cast - AKA a C-style cast. I would argue that such a cast is unworthy of a library which aspires to become bleeding edge modern C++ standard.

Furthermore, the fact that we have four different names for casts that all do the same hurts generic programming without bringing any real benefit. After all, we want the compiler to keep track of the type of variables so that we don't have to. Especially if separate technical types are conceptually/semantically similar. It is a pity that the standard library did make some precedent by creating separate duration_cast and time_point_cast, instead of a single representation_cast. But they did make a static_pointer_cast, but not a static_shared_ptr_cast and a static_unique_ptr_cast.

I therefore suggest to deprecate all of those four casts, and replace them with a single representation_cast. IMHO, that one should exclusively perform the actions listed under point 1 above. And while at it, let it apply to std::chrono types too. Note that points 2 and 3 have no relevance to std::chrono::time_point_cast, but points 4 or 5 are explicitly not supported by it.

There may potentially be a need to add a separate cast such as reinterpret_dimension or similar for points 2 and 3 above. I do not see any justification for points 4 and 5 at all, given that this can easily be achieve just by manually extracting the numerical value and re-applying it to a different quantity.

What are your thoughts?

burnpanck avatar Feb 23 '21 22:02 burnpanck

Re-interpret physical measures as fundamentally different "kind of quantities" but with shared physical dimensions. E.g. reinterpret a torque (kind of quantity) of "100 N m" into an energy (a distinct kind of quantity) of "100 N m = 100 J", where that equals sign is one of symbolic but not necessarily semantic equivalency.

For this to be true, we first need to clean up the angular mess. I have it on my TODO list but I have too much on my plate now to start it.

mpusz avatar Feb 23 '21 22:02 mpusz

What do you mean by?

Ignore the physical units of a measure and simply copy the numerical value in front of a different unit and thus into a different dimension.

mpusz avatar Feb 23 '21 22:02 mpusz

Furthermore, the fact that we have four different names for casts that all do the same hurts generic programming without bringing any real benefit.

Agree.

mpusz avatar Feb 23 '21 22:02 mpusz

let it apply to std::chrono types too

It should be a separate header and a separate ISO C++ proposal/paper.

mpusz avatar Feb 23 '21 22:02 mpusz

Actually, what I meant by 3) was the quantity_kind_cast<energy>(quantity_kind<torque>(100*N*M)) (given suitable Kinds for torque and energy), which it already does now.

By 4), I referred to quantity_cast<dim_length>(1_s). Might I have gotten that one wrong, and it's in fact prevented by the equivalent<ToD,D> requirement?

burnpanck avatar Feb 23 '21 22:02 burnpanck

As stated above right now torque is defined in terms of an angle:

https://github.com/mpusz/units/blob/8632ac1461ef3188ac748d51540b48f4fb7199b0/src/include/units/physical/si/derived/torque.h#L35

This makes it incompatible with energy and no cast will help here.

However, this should be fixed for si namespace. si namespace should be consistent with SI specs. I plan to add a new si_angle namespace that will provide respective dimensions using angle for experimentation purposes.

mpusz avatar Feb 23 '21 22:02 mpusz

By 4), I referred to quantity_cast<dim_length>(1_s). Might I have gotten that one wrong, and it's in fact prevented by the equivalent<ToD,D> requirement?

Indeed. It will not work. You cannot cast a quantity to the incompatible (not-equivalent) dimension.

mpusz avatar Feb 23 '21 22:02 mpusz

My understanding is that we have:

  • representation casting -> may truncate
  • unit casting -> may truncate when the unit has a smaller resolution or involves truncating conversion factor
  • dimension casting -> may truncate if the coherent unit of a new dimension may truncate
  • quantity casting (any of the above) -> may truncate

Am I missing something?

mpusz avatar Feb 23 '21 22:02 mpusz

"Make casts explicit in what they do, not what they apply to". This is great. This would also solve #120 by means of being incompatible. Although one could consider a quantity_cast renamed to reinterpret_quantity_cast that does it all, thereby also solving #120.

JohelEGP avatar Feb 23 '21 22:02 JohelEGP

  • kind casting -> may truncate if the coherent unit of a new dimension may truncate

Same as "dimension casting", since kinds wrap dimensions.

JohelEGP avatar Feb 23 '21 22:02 JohelEGP

I agree, they all "may truncate". What I care about is what they may not do. Given that my point 4 is moot, my whole essay here loses some of it's punch ;-). But IMHO, class 2 and 3 above; e.g. replacing "height" by "width", is erasing "physical type information". I do not want to do that accidentally.

burnpanck avatar Feb 23 '21 22:02 burnpanck

OK, I missed a "new" one 😉 Anyway, I think that all of them are of similar "severity" and I do not see a possibility to introduce multiple categories here.

mpusz avatar Feb 23 '21 22:02 mpusz

Ok, the "severity" may not be that different, but to me these are fundamentally different things on a semantic level. My understanding of modern C++ is that code should be expressive, that is it should read what it does -> semantics at the front.

burnpanck avatar Feb 23 '21 22:02 burnpanck

Anyway, with class 4 ruled out, it is indeed a matter of judgment if these things are distinct enough to warrant further division. If we can have a representation_cast I'm still happy :-).

burnpanck avatar Feb 23 '21 22:02 burnpanck

representation_cast<NewRep>(q);
representation_cast<NewUnit>(q);
representation_cast<NewDimension>(q);
representation_cast<NewKind>(q);
representation_cast<NewQuantity>(q);

I think this is expressive? I agree that having 4 names (for each type) does not have sense and we can do better. But otherwise, I do not see any other cast categories here (at least for now).

mpusz avatar Feb 23 '21 22:02 mpusz

I like the idea of giving each their name. Like I name static_downcast.

  • representation_cast<double>(1 * m)
  • unit_cast<metre>(1 * km)
  • dimension_cast<cgs::dim_length>(1 * m)
  • quantity_cast<length<metre>>(1 * km)
  • kind_cast<height>(w = 1 * m)
  • reinterpret_quantity_cast<double>(1 * m)
  • reinterpret_quantity_cast<cgs::dim_length>(1 * m)
  • reinterpret_quantity_cast<length<metre>>(1 * m)
  • reinterpret_quantity_cast<height>(w = 1 * m)
  • reinterpret_quantity_cast<time<second>>(1 * m)?

JohelEGP avatar Feb 23 '21 22:02 JohelEGP

However, I think that quantity_cast is a better name for the above, Although, I am not a native speaker.

mpusz avatar Feb 23 '21 22:02 mpusz

reinterpret_quantity_cast is a bit harsh. The current quantity_cast is more like static_quantity_cast as it is benign. It still does too many things (like static_cast), but is an improvement over a C-style cast.

JohelEGP avatar Feb 23 '21 22:02 JohelEGP

Indeed, I would reclassify the above as

  • static_cast<new_repr>(q) -> may truncate, but not reinterpret physical type information
  • static_cast<new_unit>(q) -> may truncate, but not reinterpret physical type information
  • static_cast<new_origin>(q) -> may truncate, but not reinterpret physical type information
  • reinterpret_cast<new_dimension>(q) -> does not change values and should not truncate, but does discard physical type information
  • reinterpret_cast<new_kind>(q) -> does not change values and should not truncate, but does discard physical type information Except that we may not use those names...

burnpanck avatar Feb 23 '21 22:02 burnpanck

What is the difference between representation_cast<double>(1 * m) and reinterpret_quantity_cast<double>(1 * m)?

mpusz avatar Feb 23 '21 22:02 mpusz

There is no such thing like:

reinterpret_cast<new_dimension>(q) -> does not change values and should not truncate, but does discard physical type information

Physical type information is always perceived. It is used only to cast from cgs::length to si::length and similar. Those may and will trancate if a coherent unit of a new dimension will truncate during conversion (cm -> m).

mpusz avatar Feb 23 '21 22:02 mpusz

Ah, but then we are missing something, no? How would I spell a cast that goes from quantity<si::length,metre> to quantity<cgs::length,metre> without me having to spell the metre?

burnpanck avatar Feb 23 '21 22:02 burnpanck

Physical type information is always perceived.

Not with reintrepret_cast<width>(h = 1*m).

burnpanck avatar Feb 23 '21 22:02 burnpanck

What is the difference between representation_cast<double>(1 * m) and reinterpret_quantity_cast<double>(1 * m)?

Nothing. It's just that the former only works on a specific thing as suggested by its name, whereas the latter does everything.

JohelEGP avatar Feb 23 '21 22:02 JohelEGP

reinterpret_cast<new_kind>(q) -> does not change values and should not truncate, but does discard physical type information

I am not sure about this. As @johelegp mentioned, it will also truncate for the same reason as above. Also, I do not believe that it "does discard physical type information". It retains the physical dimension and even the kind category (if I am not wrong). Just again allows casting from CGS to SI like it was the case above.

mpusz avatar Feb 23 '21 22:02 mpusz

You can cast to an unrelated quantity kind with quantity_cast.

JohelEGP avatar Feb 23 '21 22:02 JohelEGP

I thought of plausible use cases when allowing that.

  • Look at the first gif at https://en.wikipedia.org/wiki/Radian. That's transforming a radius quantity kind to a length of arc quantity kind.
  • In source code, represent a change of orientations (like mirroring) by swapping the kinds of a (width, height) pair.

JohelEGP avatar Feb 23 '21 23:02 JohelEGP

How would I spell a cast that goes from quantitysi::length,metre to quantitycgs::length,metre without me having to spell the metre?

For now, there is no cast that is able to do so as there might be no matching unit in the target system. Consider units::quantity_cast<units::physical::si::dim_acceleration>(200_q_Gal);. If you want to ensure both a target dimension and a target unit you have another quantity_cast that does exactly this and takes two parameters (ToD + ToU):

https://github.com/mpusz/units/blob/8632ac1461ef3188ac748d51540b48f4fb7199b0/src/include/units/quantity_cast.h#L192-L197

--- edit ----

I was wrong. ToD does downcast_unit<ToD, U::ratio> which will result in the same unit ratio even though it might be an unknown unit in a target system.

mpusz avatar Feb 23 '21 23:02 mpusz

What unit is Gal? I did read that as "Gallon" which I would have considered class 4...

burnpanck avatar Feb 23 '21 23:02 burnpanck

You can cast to an unrelated quantity kind with quantity_cast.

Do we need it? Maybe the constructor call to a new kind with the quantity as an argument would suffice here?

mpusz avatar Feb 23 '21 23:02 mpusz