Skip to content
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

Effectively global unit names like 'm' are problematic #558

Open
kwikius opened this issue Feb 25, 2024 · 35 comments
Open

Effectively global unit names like 'm' are problematic #558

kwikius opened this issue Feb 25, 2024 · 35 comments

Comments

@kwikius
Copy link
Contributor

kwikius commented Feb 25, 2024

The use of names such as 'm' effectively in the global namespace doesn't scale well. I have been looking into trying to use mp-units in a real library and selected ArduPilot, since I know it well. I did a search for ' m ' in the codebase in .cpp and .h files and came up with around 350 hits. m is of course only one of many very short names which are brought in by the using namespace mp_units::si::unit_symbols declaration.

EDIT: for example :

#include <mp-units/systems/si/si.h>
#include <iostream>
#include <mp-units/format.h>
#include <mp-units/ostream.h>

using namespace mp_units::si::unit_symbols;

int main()
{
    auto q1 = 1 * m;

    std::cout << q1 <<'\n';

    int m = 1;

    auto q2 =  2 * m;

    std::cout << q2 <<'\n';
}

The rough solution I came up with was a namespace alias, but is there another official alternative way to initialise quantities than these very unit names, because I can see them being very problematic if you are trying to update an existing codebase and this is the only way to initialise a quantity.

#include <mp-units/systems/si/si.h>
#include <iostream>
#include <mp-units/format.h>
#include <mp-units/ostream.h>

namespace si_unit = mp_units::si::unit_symbols;

int main()
{
    static_assert(10.0 * si_unit::m / 2 == 5 * si_unit::m );

    auto q1 = 10.0f * si_unit::m;

    std::cout << q1 <<'\n';
    auto q2 = q1/q1;

    std::cout << q2 <<'\n';

}

Below is a small selection of the results of the search

~/ardupilot/libraries/SITL/SIM_Aircraft.cpp:436:            Matrix3f m = dcm;
~/ardupilot/libraries/SITL/SIM_Aircraft.cpp:437:            m = m * sitl->ahrs_rotation_inv;
~/ardupilot/libraries/SITL/SIM_XPlane.cpp:500:                    uint32_t m = uint32_t(data[7]) & j->mask;
~/ardupilot/libraries/AP_Declination/tests/test_magfield.cpp:119:        const Vector3f m = AP_Declination::get_earth_field_ga(loc);
~/ardupilot/libraries/AP_HAL_SITL/DSP.cpp:174:    uint16_t m = fft_log2(fftlen);
~/ardupilot/libraries/AP_Filesystem/AP_Filesystem_Mission.cpp:420:        mavlink_mission_item_int_t m {};
~/ardupilot/libraries/AP_Filesystem/AP_Filesystem_Mission.cpp:476:        mavlink_mission_item_int_t m {};
~/ardupilot/libraries/AP_Filesystem/AP_Filesystem_Mission.cpp:513:        mavlink_mission_item_int_t m {};
~/ardupilot/libraries/AP_Common/time.cpp:25:    m = 0;
~/ardupilot/libraries/AP_Common/time.cpp:34:            m = 0;
~/ardupilot/libraries/AP_RCTelemetry/AP_Spektrum_Telem.cpp:567:    m = 0;
~/ardupilot/libraries/AP_Math/tests/test_rotations.cpp:136:        vec2 = m * vec2;
~/ardupilot/libraries/AP_Math/examples/rotations/rotations.cpp:322:        vec2 = m * vec2;
~/ardupilot/libraries/AP_Math/examples/eulers/eulers.cpp:227:    v2 = m * v;
~/ardupilot/libraries/AP_Math/examples/eulers/eulers.cpp:233:    v2 = m * v;
~/ardupilot/libraries/AP_Math/examples/eulers/eulers.cpp:239:    v2 = m * v;
~/ardupilot/libraries/AP_Math/quaternion.cpp:416:    v = m * v;
~/ardupilot/libraries/AC_Avoidance/AP_OABendyRuler.cpp:697:        const float m = Vector3f::closest_distance_between_line_and_point(start_NEU, end_NEU, point_cm) * 0.01f - item.radius;
@chiphogg
Copy link
Collaborator

I think the solution is to follow general C++ best practices, and avoid blanket using directives in favor of individual using declarations. That is, in each individual .cc file, we would write:

using mp_units::si::unit_symbols::m;

...and similarly for every other symbol we want to incorporate. This has the advantage that it's easy to see where each symbol comes from, and of course also that it doesn't bring in a host of other symbols that we didn't think about.

If we're in a file that has already used m for another purpose, we simply wouldn't use the unit symbol m in that file.

@mpusz
Copy link
Owner

mpusz commented Feb 25, 2024

As stated in https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html#constructing-a-quantity:

Unit symbols introduce a lot of short identifiers into the current scope, which is why they are opt-in. A user has to explicitly “import” them from a dedicated unit_symbols namespace.

In case "importing" unit_symbols is a no-go, one can use many solutions:

  • just use 42 * si::metre :-)
  • using directive to bring only selected units to the scope,
  • provide custom variables in case of collisions constexpr auto M = si::metre; auto q = 42 * M;

In the V2 design, we have plenty of options to choose from.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 25, 2024

I think the solution is to follow general C++ best practices, and avoid blanket using directives in favor of individual using declarations.

Maybe then best to rewrite the first example I came across to follow c++ best practice?

@kwikius
Copy link
Contributor Author

kwikius commented Feb 25, 2024

As stated in https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html#constructing-a-quantity:

Unit symbols introduce a lot of short identifiers into the current scope, which is why they are opt-in. A user has to explicitly “import” them from a dedicated unit_symbols namespace.

In case "importing" unit_symbols is a no-go, one can use many solutions:

* just use `42 * si::metre` :-)

* using directive to bring only selected units to the scope,

* provide custom variables in case of collisions `constexpr auto M = si::metre; auto q = 42 * M;`

In the V2 design, we have plenty of options to choose from.

The ctor syntax I would recommend is the quantity{value,unit} , but then I find myself thinking, I might just as well do it in a terser syntax:

#include <mp-units/systems/si/si.h>
#include <iostream>
#include <mp-units/format.h>
#include <mp-units/ostream.h>

#define MAKE_QUANTITY(Quantity, Numeric, Unit, Mp_unit) \
namespace Quantity {\
      struct Unit : decltype( mp_units::quantity{ Numeric() , Mp_unit })\
      {\
         explicit Unit( Numeric v) : quantity{v, Mp_unit }{}\
      };\
}

namespace pqs {
    MAKE_QUANTITY(velocity ,double ,m_per_s ,mp_units::si::metre/mp_units::si::second )
    MAKE_QUANTITY(time ,double ,s ,mp_units::si::second )
}

int main()
{
   auto q1 = pqs::velocity::m_per_s{20};

   auto q2 = pqs::time::s{2};

   pqs::time::s q3 = q2;

   std::cout << q1 * q2 <<'\n';

   std::cout << q3 <<'\n';

   return 0;
}

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

Maybe then best to rewrite the first example I came across to follow c++ best practice?

I do not think we have to rewrite the examples to use using-declarations. Using-directive is also fine in this case. In production code, we convert raw values to quantities in only a few places, typically only in a few functions. In such cases, it is often perfectly fine to use using-directives inside of such a function, and in case of naming conflicts, it doesn't hurt to spell the entire unit as we do not do this too often. In a production code, it is typically a mistake to use using-directives for the entire file scope, and we should nearly never do this inside of a header file. We have a lot of practice with using namespace std; and we should do the same with any other namespace as well.

An exception to this rule is typically unit tests, where we often create quantities from values. In such cases, using-directive is useful for the entire file as long as it does not produce hard-to-deal-with conflicts.

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

The ctor syntax I would recommend is the quantity{value,unit} , but then I find myself thinking, I might just as well do it in a terser syntax:

I played a lot with the syntax you proposed in V1 and spoke about it on conferences as well (https://youtu.be/Nt7B1vI8DtI?si=W_loEqQkageu5dHT&t=2329). There a few big issues with it where the most important ones are:

  • it does not compose
  • requires a redefinition for the same unit used in different quantities

Each of the syntaxes I described in the V1 had quite a lot of issues. I believe that the solution I came up for V2 is much better from anything that we had before.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 26, 2024

Composability of units https://github.com/kwikius/pqs/blob/master/examples/atmosphere.cpp#L17 and dimensions https://github.com/kwikius/pqs/blob/master/src/include/pqs/types/derived_quantity/acceleration.hpp#L9 were feature of pqs when mp-units V2 was a gleam in its authors eye I believe, with a similar syntax. I may be wrong about that but I seem to remember a variadic template and some heavyweight metaprogramming was required in mp-units V1 . If I got that wrong please correct me and I will apologise , so composability is not a problem here.

But the thread is specifically about the issues arising from using effectively short global names to initialise a quantity which is a consequence of basically preventing the user from value initialising a quantity, unless supplying the units. For example the identifiers in
https://github.com/mpusz/mp-units/blob/master/example/capacitor_time_curve.cpp
have had to be renamed from
https://github.com/kwikius/quan-trunk/blob/master/quan_matters/examples/capacitor_time_curve.cpp
So it is a real issue in real code. Someone will have to go through and rename to prevent collisions, some of which may not show up until later in the code, and with quite long compile times for mp-units for some reason. It doesnt help to 'sell" a quantities library.
You say, don't do use short names in real code ( or do it but realise the implications) and I agree but then the examples used throughout the documentation are somewhat disingenuous. They should at least provide a note close by to warn about it.

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

If I got that wrong please correct me and I will apologise , so composability is not a problem here.

There is no need to apologize. We are not arguing or fighting here. We all want to find the best solution, and we need to discuss different approaches.

Said that, I do not agree with that statement. How do you achieve pqs::velocity::m_per_s in the user's code in case it is not predefined in the library? Composability does not mean that you may use a division operator to decltype the result. What I mean by "composability" is that the user can use any unit with a user-friendly syntax without the need for such a unit/dimension/quantity to be predefined in the library. For example:

  • auto q = 42 * m / s,
  • auto q = 42 * si::metre / si::second,
  • constexpr auto mps = m / s; auto q = 42 * mps;,
  • void foo(quantity<si::metre / si::second>);,
  • void foo(quantity<isq::speed[si::metre / si::second]>);,
  • void foo(quantity<isq::velocity[si::metre / si::second]>);.

But the thread is specifically about the issues arising from using effectively short global names to initialise a quantity which is a consequence of basically preventing the user from value initialising a quantity, unless supplying the units.

The usage of short global names has nothing in common with preventing the user from value initializing a quantity. Those are totally orthogonal. We would use the latter no matter of the way we spell the unit. The latter is about safety, and we believe it is the right way to go. Plenty of people (including the ISO C++ Committee experts) warned us about the safety issues that arise from the similar approach in std::chrono::duration. This is why we do not allow such an initialization. See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html#explicit-is-not-explicit-enough for more details. It is also clearly stated in our docs: https://mpusz.github.io/mp-units/latest/getting_started/faq/#why-cant-i-create-a-quantity-by-passing-a-number-to-a-constructor.

quite long compile times for mp-units for some reason

Yes, we will need to optimize compile times at some point. Until now, I have not spent any time on improving those as we still struggle to address the last missing pieces of the design, which have priority now. This library was designed from the very beginning with the C++ modules in mind, and support for C++ modules is arriving just now. Hopefully, soon, we will have some time and the possibility to work on the compile time performance.

They should at least provide a note close by to warn about it.

This is why we describe this in one of the first chapters of your documentation: https://mpusz.github.io/mp-units/latest/getting_started/quick_start/#quantities. We may extend that admonition with more details if you think it is needed.

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

I've just extended the documentation here: https://mpusz.github.io/mp-units/latest/getting_started/quick_start/#quantities.

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

A side note on the compilation performance

It is true that we can optimize the compilation performance, and we will do it for sure at some time in the future. However, usage examples compile so slow because of several additional reasons:

  • <format> header file is really slow to compile by itself, and the experience highly depends on the vendor:
    • LLVM header compiles 2x slower than the fmtlib one
    • GCC header compiles 2x slower than the LLVM one
  • "trunk" compilers on the Compiler Explorer are much slower than the versioned ones (a different server's mass storage is being used to host them). We need to use clang-trunk to get a few features for now (e.g., std::print). However, those should arrive in clang-18, which will be released soon. I will update the links for all the samples after this release to use a versioned compiler, so hopefully, the usage experience of our samples will improve soon as well.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 26, 2024

The latter is about safety, and we believe it is the right way to go. Plenty of people (including the ISO C++ Committee experts) warned us about the safety issues that arise from the similar approach in std::chrono::duration. This is why we do not allow such an initialization. See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html#explicit-is-not-explicit-enough for more details.

It is a good example, but again with short unit names particularly it is not infallible

#include <mp-units/systems/si/si.h>
#include <iostream>
#include <mp-units/format.h>
#include <mp-units/ostream.h>
#include <vector>

using namespace mp_units::si::unit_symbols;

namespace si = mp_units::si;

int main()
{
   auto constexpr  q1 = 1 * m;

   auto constexpr m = q1 - q1;

   std::vector< mp_units::quantity<si::metre> > v;

   v.emplace_back( 42 * m );

   std::cout << q1 / v[0] <<'\n';

   return 0;
}

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

Sure! unit_symbols are just a construction helper and there are other ways to achieve the same goal without them. In cases where they cause issues, they should not be used. They are never implicitly provided by default. A user always has to explicitly opt-in to use them and should be aware of potential consequences.

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

We could not provide those at all and leave the definition of short names to the user. It would even improve compile times a lot as this is the biggest header file to process right now (https://github.com/mpusz/mp-units/blob/master/src/systems/include/mp-units/systems/si/unit_symbols.h).

However, there are many places where they can be used and improve the usability and readability of code. This is why we decided to provide them with the library.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 26, 2024

Compilation time is probably a discussion for another issue, so I should open that now

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

Sure, please do. Although it may take a while before we start tuning up the compile-time performance.

@mpusz
Copy link
Owner

mpusz commented Feb 26, 2024

Is there anything else you would like to discuss in this issue? Can we improve something here besides the docs that I've updated already?

@chiphogg
Copy link
Collaborator

I think the green "Important" block should be improved, yes. We currently list the "using directive" approach first, and without comment. This makes it look like we're recommending it! I think we should instead list it last, and explicitly say that we don't recommend the approach in production code, and give a couple of reasons why.

Another big advantage of unit symbols over user-defined literals is that we no longer need to recommend the using directive approach.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 26, 2024

I would basically concur with @chiphogg . When I start reading the documentation I would like to have those short names put in some sort of context , and show intended best practice at least at first encounter in the documentation. Later perhaps this could be abbreviated, though every page is an entry point for somebody.

I modified the code to get it to work with gcc. The anonymous namespace is intended to show file scope so the units could be used in other functions in the source file.. YMMV

#include <format>
#include <iomanip>
#include <iostream>
#include <mp-units/format.h>
#include <mp-units/ostream.h>
#include <mp-units/systems/si/si.h>
#include <mp-units/systems/international/international.h>

using namespace mp_units;

constexpr QuantityOf<isq::speed> auto avg_speed(QuantityOf<isq::length> auto d,
                                                QuantityOf<isq::time> auto t)
{
  return d / t;
}

namespace {
   // comments here 
   using si::unit_symbols::km;
   using si::unit_symbols::m;
   using si::unit_symbols::h;
   using si::unit_symbols::s;

  // etc
   using international::unit_symbols::mph;
//etc
   using international::unit_symbols::mi;

}

int main()
{
   constexpr quantity v1 = {110 , km / h};
   constexpr quantity v2 = 70 * mph;  // alternate form
   constexpr quantity v3 = avg_speed(quantity{220., isq::distance[km]}, quantity{2 , h});
   constexpr quantity v4 = avg_speed(isq::distance(140. * mi), 2 * h);  // alternate form
   constexpr quantity v5 = v3.in(m / s);
   constexpr quantity v6 = value_cast<m / s>(v4);
   constexpr quantity v7 = value_cast<int>(v6);

   std::cout << v1 << '\n';                                        // 110 km/h
   std::cout << std::setw(10) << std::setfill('*') << v2 << '\n';  // ***70 mi/h
   std::cout << std::format("{:*^10}\n", v3) <<'\n';               // *110 km/h*
   std::cout << std::format("{:%N in %U of %D}", v4)<<'\n';        // 70 in mi/h of LT⁻¹
   std::cout << std::format("{:{%N:.2f}%?%U}", v5)<<'\n';          // 30.56 m/s
   std::cout << std::format("{:{%N:.2f}%?{%U:dn}}", v6)<<'\n';     // 31.29 m⋅s⁻¹
   std::cout << std::format("{:%N}", v7)<<'\n';                    // 31
}

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

Agreed, I will review the docs and think about how to improve the examples.

However, I do not think that there is anything wrong with using-directives if they are used in a limited (e.g., function) scope. So, in the example above, it is perfectly fine to add those to the main() function without the need to explicitly bring all of them one by one in the namespace.

I even think that bringing them to the namespace is as bad as similar usage of using-directives as they may collide in some unforeseen places anyway. Raw numbers are (or at least should be) converted to quantities only in a few specific places in the project, and having unit symbols accessible in the entire program through its namespace is encouraging to do wrong things as well and may easily backfire.

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

I just finished the review of all the examples in the documentation. Using-directive was nearly never used in the global/namespace scope, so we are fine.

Additionally, I improved the "Important" admonition in the docs to warn against the usage of using-directives in a wide scope.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

I even think that bringing them to the namespace is as bad as similar usage of using-directives as they may collide in some unforeseen places anyway. Raw numbers are (or at least should be) converted to quantities only in a few specific places in the project, and having unit symbols accessible in the entire program through its namespace is encouraging to do wrong things as well and may easily backfire

In the above example I used specifically an anonymous (unnamed) namespace , which emphasises that the names have internal linkage, so are only available in the particular translation unit, they are defined in.

but the example here is enough for me to recommend avoiding this style as far as possible.

Bringing the entire unit short name set into a large function is arguably as bad. You have the opposite problem. One day a new unit may be added to the namespace and quietly hide a value from an outer scope

FWIW I don't know if this is correct behaviour in the following (gcc-13) , but if you uncomment the SOMETHING define assert fires, and not if you don't. And this is why I probably will recommend not to use any of these short unit aliases!

#include <format>
#include <iomanip>
#include <iostream>

#include <mp-units/format.h>
#include <mp-units/ostream.h>
#include <mp-units/systems/si/si.h>
#include <mp-units/systems/international/international.h>

#include <cassert>

using namespace mp_units;

#define SOMETHING

void f(){
//...
#if defined SOMETHING
  quantity km = { 1.e-3, si::metre};
#endif
   //...
   {
      using namespace si::unit_symbols;
      auto q2 = 10 * km;
      assert( q2.unit == si::kilo<si::metre>);
   }
}

int main()
{
   f();
}

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

FWIW in pqs a unit is composable using op * , op / , etc which returns a unit but there is no multiplication of a unit by a number.

in using mp-units I would recommend always using the mp-units value constructor that keeps the number and the unit separate:

auto q =  quantity{number, unit};

Interestingly, in #558 (comment) if I replace

     auto q2 = 10 * km;

with

      quantity q2 = { 10, km };

I get an error message when SOMETHING is defined and that seems to be a big advance

EDIT: In fact if I got around to reworking pqs, I think that {number, unit} is the value ctor I would opt for. It allows composability of math.numbers and of physical.units while keeping them in their separate domains, and the quantity constructor is a 'black box' which unites the domain of maths and physics in a quantity system

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

FWIW I don't know if this is correct behaviour in the following (gcc-13) , but if you uncomment the SOMETHING define assert fires, and not if you don't.

Sure, this is a correct behavior. If you define a local variable km (with the value of 1 mm in this case, which is quite confusing indeed 😉), then it will be used instead of external symbols brought with using directive. What is interesting is that using declaration would work correctly here.

And this is why I probably will recommend not to use any of these short unit aliases!

Sure, if you want to do non-trivial logic in your function, it is perfectly fine not to use symbols and stick with full unit names.
I think I stated this clearly in https://mpusz.github.io/mp-units/latest/getting_started/quick_start?

Also, you probably should not recommend using auto 😉 as there are other possible issues here:

auto v = 10 * si::kilo<si::metre>;
auto q = v * si::kilo<si::metre>;

q above will end up as the quantity of area 😢 as discussed here: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html#potential-surprises-during-units-composition. It was safer before, but I got downvoted.

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

We could consider an alternative approach here. Instead of having to do using namespace si::unit_symbols we could reorder the namespaces and provide using namespace unit_symbols::si. This would allow us to have a "safer mode":

using namespace unit_symbols;
auto q = 42 * si::m;

which would be much harder to hijack. Also, this would allow us to easier disambiguate between si::s and cgs::s.

The only downside here would be a bit harder definition of symbols as we would need to exit the mp_units::si namespace first in order to define them. And we would break our existing users as well... 😢

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

Sure, this is a correct behavior. If you define a local variable km < ... >, then it will be used instead of external symbols brought with using directive. What is interesting is that using declaration would work correctly here.

I don't want to have to be clever and trip up my users and then tell them how dumb they are for not knowing the intricate rules at work here. I consider myself a reasonably competent C++ programmer and I didn't know the rules here.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

auto v = 10 * si::kilo<si::metre>;
auto q = v * si::kilo<si::metre>;
    

Sure , units shouldn't be mixed with numbers. They are different domains. This is the best approach for clarity.
Dump the multiply number by unit syntax!

   auto speed_unit = mp_units::si::metre / mp_units::si::second;

   mp_units::quantity q = {20.0f, speed_unit};

   float value = q.numerical_value_in(q.unit);

   std::cout << value << '\n';

   auto area_unit = mp_units::si::metre * mp_units::si::metre;

   mp_units::quantity q1 = {10, area_unit};

   value = q1.numerical_value_in(q1.unit);

   std::cout << value << '\n';

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

Dump the multiply number by unit syntax!

This probably is not going to happen because the constructor syntax does not compose. Just compare the two definitions:

quantity q1 = quantity{7, non_si::hour} + quantity{20, non_si::minute} + quantity{35, si::second};
quantity q2 = 7 * h + 20 * min + 35 * s;

The second one is so much easier to use.

However, as it is clearly stated in our docs (https://mpusz.github.io/mp-units/latest/getting_started/quick_start/#quantities), the multiply syntax is not mandatory, and explicit constructor call may be used instead. The same applies to unit_symbols, which are not provided by default.

Users have the choice to decide which style best suits their needs.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

q above will end up as the quantity of area 😢 as discussed here: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3045r0.html#potential-surprises-during-units-composition. It was safer before, but I got downvoted.

Yes. All the problems there are due to being able to multiply numbers ( and quantities) by units. Neither quantities nor number should be composable with units, I think the practise and syntax started with Walter Browns SI units library. https://lss.fnal.gov/archive/1998/conf/Conf-98-328.pdf .

Anyway, there is an alternative value ctor provided so I will try using that exclusively and see how that goes.

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

I would actually be really interested in hearing some feedback from you when you are done.

My understanding is that conversions from unsafe raw values to quantities should happen only in a few entry points to a program, and those should be limited to a very narrow scope to prevent potential safety issues. Mixing unsafe raw quantity values and strong quantities in the same context might be a recipe for disaster.

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

This probably is not going to happen because the constructor syntax does not compose. Just compare the two definitions:

quantity q1 = quantity{7, non_si::hour} + quantity{20, non_si::minute} + quantity{35, si::second};
quantity q2 = 7 * h + 20 * min + 35 * s;

You are being somewhat unfair to the 2 arg ctor here. Short names for units would be much more acceptable if they didnt compose with numbers and quantities so what is good for the goose is good for the gander

// may be a better way to do this
  auto q = []( auto v, auto U) { return quantity {v, U };};
  quantity q1 = q(7, h) + q(20, min) + q(35, s);
  quantity q2 = 7 * h + 20 * min + 35 * s;

  std::cout << " q1 = " << q1 <<'\n';
  std::cout << " q2 = " << q2 <<'\n';

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

I would actually be really interested in hearing some feedback from you when you are done.

My understanding is that conversions from unsafe raw values to quantities should happen only in a few entry points to a program, and those should be limited to a very narrow scope to prevent potential safety issues. Mixing unsafe raw quantity values and strong quantities in the same context might be a recipe for disaster.

You are going to have to write some quite dirty code, to move a legacy library from one to the other. Ardupilot is a great example of using floats for everything and you spend a lot of time checking units . ( Bear in mind ArduPilot is huge and I suspect there would be a lot of resistance to change)

Here is one part that might be of interest to you . ( Ardupilot can do autonomous thermal soaring)
https://github.com/ArduPilot/ardupilot/blob/master/libraries/AP_Soaring/Variometer.h

In comparison to

https://github.com/mpusz/mp-units/blob/master/example/glide_computer_lib/include/glide_computer_lib.h

@kwikius
Copy link
Contributor Author

kwikius commented Feb 27, 2024

The killer application for mp-units and ardupilot would be if use of units would increase speed and reduce code size. I am pretty sure that it can FWIW. There are piecemeal efforts to streamline units e.g ArduPilot/ardupilot#25768 . Ardupilot uses degrees, centidegrees and radians pretty much randomly throught out the codebase.

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

You are going to have to write some quite dirty code, to move a legacy library from one to the other.

I am not sure what the ArduPilot code looks like. If it has some strong type wrappers (e.g., AP_Float), then the following chapter might be a good start for migration: https://mpusz.github.io/mp-units/latest/users_guide/use_cases/interoperability_with_other_libraries.

@mpusz
Copy link
Owner

mpusz commented Feb 27, 2024

Ardupilot is a great example of using floats for everything and you spend a lot of time checking units .

Yes, I totally understand the issue, as I contributed for a few years to the http://lk8000.it. It was 15 years ago, and this is when I realized how important it is to solve the issue of units in similar code bases. I work on it from then...

@JohelEGP
Copy link
Collaborator

JohelEGP commented Mar 6, 2024

We could consider an alternative approach here. Instead of having to do using namespace si::unit_symbols we could reorder the namespaces and provide using namespace unit_symbols::si. This would allow us to have a "safer mode":

using namespace unit_symbols;
auto q = 42 * si::m;

which would be much harder to hijack. Also, this would allow us to easier disambiguate between si::s and cgs::s.

The only downside here would be a bit harder definition of symbols as we would need to exit the mp_units::si namespace first in order to define them. And we would break our existing users as well... 😢

Promote the use of namespace si = mp_units::si::unit_symbols; instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants