mp-units
mp-units copied to clipboard
Make casts explicit in what they do, not what they apply to
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:
- 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
toint
- 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.
- A change of the binary representation of the value, e.g.
- 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.
- 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.
- (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. - (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?
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.
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.
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.
let it apply to std::chrono types too
It should be a separate header and a separate ISO C++ proposal/paper.
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?
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.
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.
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?
"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.
- kind casting -> may truncate if the coherent unit of a new dimension may truncate
Same as "dimension casting", since kinds wrap dimensions.
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.
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.
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.
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 :-).
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).
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)
?
However, I think that quantity_cast
is a better name for the above, Although, I am not a native speaker.
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.
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...
What is the difference between representation_cast<double>(1 * m)
and reinterpret_quantity_cast<double>(1 * m)
?
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).
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
?
Physical type information is always perceived.
Not with reintrepret_cast<width>(h = 1*m)
.
What is the difference between
representation_cast<double>(1 * m)
andreinterpret_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.
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.
You can cast to an unrelated quantity kind with quantity_cast
.
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.
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.
What unit is Gal
? I did read that as "Gallon" which I would have considered class 4...
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?