Search code examples
c++c++11boostboost-units

How to use Boost.Unit with ratios in the same unit


I have a very simple use case for using Boost.Unit, but not sure if there is a better/easier way to get the same done.

I want to convert between the same units, but different ratios. For example, hertz to kilohertz to megahertz.

From my understanding, I first must define units with my specific ratios:

typedef boost::units::make_scaled_unit<si::frequency, scale<10, static_rational<0> > >::type  Hertz_unit;
typedef boost::units::make_scaled_unit<si::frequency, scale<10, static_rational<3> > >::type  KilloHertz_unit;
typedef boost::units::make_scaled_unit<si::frequency, scale<10, static_rational<6> > >::type  MegaHertz_unit;

Then create quantities that represent the units:

typedef boost::units::quantity<Hertz_unit     , double> Hertz;
typedef boost::units::quantity<KilloHertz_unit, double> KilloHertz;
typedef boost::units::quantity<MegaHertz_unit , double> MegaHertz;

Finally some constants and literals:

BOOST_UNITS_STATIC_CONSTANT( Hz, Hertz_unit     );
BOOST_UNITS_STATIC_CONSTANT(KHz, KilloHertz_unit);
BOOST_UNITS_STATIC_CONSTANT(MHz, MegaHertz_unit );

Hertz      operator"" _Hz  (long double val) { return Hertz     (val *  Hz); }
KilloHertz operator"" _KHz (long double val) { return KilloHertz(val * KHz); }
MegaHertz  operator"" _MHz (long double val) { return MegaHertz (val * MHz); }

Now I can use the quantities:

Hertz      freq_1 = (10 *  Hz);
KilloHertz freq_2 = (10 * KHz);
MegaHertz  freq_3 = (10 * MHz);
// OR
Hertz      freq_4 = 10.0_Hz;
KilloHertz freq_5 = 10.0_KHz;
MegaHertz  freq_6 = 10.0_MHz;
// Convert between units
Hertz freq_7 = static_cast<Hertz>(10 * KHz);

Is this how Boost.Unit should be used or am I missing something that might make it easier to use?

Are there not already defined units/quantities that I can use somewhere hidden in a header? Or should this be done for all my units that I use?

Do I need to know/remember that Kilo is scale<10, static_rational<3> or is this already defined and available?


Solution

  • There are a few different predefined "systems" that make things easier to use and avoid needing to define your own units and scales.

    While this code doesn't involve frequencies, you should be able to adapt it to your needs (there is a boost/units/systems/si/frequency.hpp header, for example):

    #include <boost/units/quantity.hpp>
    #include <boost/units/systems/si/length.hpp>
    #include <boost/units/systems/si/prefixes.hpp>
    
    using boost::units::si::meters;
    using boost::units::si::milli;
    
    typedef boost::units::quantity<boost::units::si::length> length;
    static const auto millimeters = milli * meters;
    
    // ...
    
    auto x = length(5 * millimeters);
    auto mm = double(x / meters * 1000.0);
    

    You used to be able to do this without the explicit cast to length (although you then needed to explicitly type the variable as length instead of using auto), but at some point this was made to require an explicit cast.

    In theory you shouldn't need to do the conversion from meters to mm manually in that second line, but the obvious construction x / millimeters produces compile errors that I never did manage to figure out a good workaround (the scale doesn't cancel out like it should).

    (You can also use x.value() rather than x / meters, but I don't like that approach as it will still compile and give you surprising results if the base unit of x wasn't what you were expecting. And it still doesn't solve the mm conversion issue.)

    Alternatively you might want to consider something like this answer, although that's mostly geared to using a single alternative scale as your base unit.


    Here's another method using frequencies and multiple quantity types:

    #include <boost/units/quantity.hpp>
    #include <boost/units/systems/si/frequency.hpp>
    #include <boost/units/systems/si/prefixes.hpp>
    
    using boost::units::si::hertz;
    using boost::units::si::kilo;
    using boost::units::si::mega;
    
    static const auto kilohertz = kilo * hertz;
    static const auto megahertz = mega * hertz;
    
    typedef boost::units::quantity<boost::units::si::frequency> Hertz;
    typedef boost::units::quantity<decltype(kilohertz)> KiloHertz;
    typedef boost::units::quantity<decltype(megahertz)> MegaHertz;
    
    // ...
    
    auto freq_1 = Hertz(10 * hertz);
    auto freq_2 = KiloHertz(10 * kilohertz);
    auto freq_3 = MegaHertz(10 * megahertz);
    auto freq_4 = KiloHertz(freq_3);
    // freq1.value() == 10.0
    // freq2.value() == 10.0
    // freq3.value() == 10.0
    // freq4.value() == 10000.0
    

    You can do conversions and maths on these fairly easily; in this context the value() is probably the most useful as it will naturally express the same unit as the variable.

    One slightly unfortunate behaviour is that the default string output for these units is presented as inverse seconds (eg. freq2 is "10 k(s^-1)"). So you probably just want to avoid using those.

    And yes, the operator"" works as well, so you could substitute these in the above:

    Hertz     operator"" _Hz(long double val) { return Hertz(val * hertz); }
    KiloHertz operator"" _kHz(long double val) { return KiloHertz(val * kilohertz); }
    MegaHertz operator"" _MHz(long double val) { return MegaHertz(val * megahertz); }
    
    auto freq_1 = 10.0_Hz;
    auto freq_2 = 10.0_kHz;
    auto freq_3 = 10.0_MHz;
    auto freq_4 = KiloHertz(freq_3);
    

    For consistency you can also define Hertz in terms of hertz, but due to quirks it's a little more tricky than the others; this works though:

    typedef boost::units::quantity<std::decay_t<decltype(hertz)>> Hertz;
    typedef boost::units::quantity<std::decay_t<decltype(kilohertz)>> KiloHertz;
    typedef boost::units::quantity<std::decay_t<decltype(megahertz)>> MegaHertz;