How to handle units in c++ interface
Asked Answered
K

9

21

I am currently designing an API where I want that the user to be able to write code like this:

PowerMeter.forceVoltage(1 mV);
PowerMeter.settlingTime(1 ms);

Currently we do this using defines like:

#define mV *1.0e-03

This makes it very convenient for the user to write their code and it is also very readable, but of course has also drawbacks:

int ms;

Will throw some compiler errors which are hard to understand. So I am looking for a better solution.

I tried the new C++11 literals, but with this all I could achieve is:

long double operator "" _mV(long double value) {
  return value * 1e-3;
}
PowerMeter.forceVoltage(1_mV);

In the end the API does not care about the unit like Volt or second but only takes the number, so I don't want to do any checking if you really input Volts in forceVoltage or not. So this should also be possible:

PowerMeter.forceVoltage(2 ms);

Any idea besides staying with the defines?

Katerinekates answered 14/6, 2012 at 6:9 Comment(6)
Can you pass your units as a separate variable? PowerMeter.forceVoltage(2, "ms"); Or maybe the entire expression as a string?Fistic
I could do this, but it is not the natural way the user would like to program.Katerinekates
Why... So you want to specify measure units but don't want to check they're correct? It doesn't make sense. Furthermore, your 'user' knows C++ but cares about 'natural way'?.. Almost unthinkable. Why not just include it to function name?Kidderminster
No, actually most of the users are not really c++ programmers. Thus we want to keep the API simple. Including in the function name is dificult, because than we would have forceVolt() forceMiliVolt() ...Katerinekates
Note that all you are doing is attaching SI prefixes, not units (milli := 1e-03 is not a unit, it's a value/prefix).Glamorize
I really think You should check mr Stroustrup lecture: channel9.msdn.com/Events/GoingNative/GoingNative-2012/… (check at 18m45s and further)Oreste
L
2

I prefer avoiding macros where ever I can, and this is an example where it should be possible. One lightweight solution that gives you correct dimensions would be:

static double m = 1;
static double cm = 0.1;
static double mV = 0.001;

double distance = 10*m + 10*cm;

This also reflects the physical concept that units are something that's multiplied with the value.

Laughingstock answered 14/6, 2012 at 12:10 Comment(2)
Additional comment: You might want to use a namespace for this, and the user of your API can then decide if he prefers cluttering his namespace or doing a setVoltage(10 * CoolUnits::m). But still better than defining stuff.Laughingstock
Cute with little overhead, but its not enough. The problem with this is that $1m \neq 1$, but to $m$. That is, $1m = 100 cm$ but the unit themselves are also a term. This is an issue when a person tries to add a temperature to a distance. Your solution would allow it.Drumstick
F
18

how about instead turning it around a bit by creating classes (ms,mV) for the different currents

e.g.

PowerMeter.forceVoltage( mV(1) );  
PowerMeter.settlingTime( ms(1) )

It is pretty clear to the user and arguably not hard to read plus you would get type checking for free. having a common base class for the different units would make it easier to implement.

Forjudge answered 14/6, 2012 at 6:29 Comment(1)
Boost.Date_Time uses this approach for its time_duration types (boost.org/doc/libs/1_49_0/doc/html/date_time/…).Calvinna
S
9

You can see the library "C++ Units" from Calum Grant as a good example of how to implement this. The library is a bit outdated, but still worth to see or may be to use.

Also, i think it might be interesting to read: "Applied Template Metaprogramming in SI UNITS: the Library of Unit-Based Computation"

There is one more good library: UDUNITS-2 which:

contains a C library for units of physical quantities and a unit-definition and value-conversion utility.

Seibert answered 14/6, 2012 at 6:32 Comment(1)
+1 Beyond ratios, the dimensions are the most important parts. With the dimensions settled, the ratios (almost) come for free.Poyang
I
8

You could use C++11's compile-time rational arithmetic support for the units, instead of defining literals or macros for the units.

Intorsion answered 14/6, 2012 at 6:20 Comment(0)
C
6

Take a look at Boost.Units. Here's some example code:

quantity<energy>
work(const quantity<force>& F, const quantity<length>& dx)
{
    return F * dx; // Defines the relation: work = force * distance.
}

...

/// Test calculation of work.
quantity<force>     F(2.0 * newton); // Define a quantity of force.
quantity<length>    dx(2.0 * meter); // and a distance,
quantity<energy>    E(work(F,dx));  // and calculate the work done.
Carnelian answered 14/6, 2012 at 14:5 Comment(1)
There's also PhysUnits-CT-Cpp11, a small C++11, C++14 header-only library for compile-time dimensional analysis and unit/quantity manipulation and conversion. Simpler than Boost.Units, only depends on standard C++ library, SI-only, integral powers of dimensions.Parttime
C
2

Here's what I came up with... pretty much the same idea as Anders K, but since I wrote the code, I'll post it:

#include <iostream>

using namespace std;

class MilliVoltsValue;
class VoltsValue;

class VoltsValue
{
public:
   explicit VoltsValue(float v = 0.0f) : _volts(v) {/* empty */}
   VoltsValue(const MilliVoltsValue & mV);

   operator float() const {return _volts;}

private:
   float _volts;
};

class MilliVoltsValue
{
public:
   explicit MilliVoltsValue(float mV = 0.0f) : _milliVolts(mV) {/* empty */}
   MilliVoltsValue(const VoltsValue & v) : _milliVolts(v*1000.0f) {/* empty */}

   operator float() const {return _milliVolts;}

private:
   float _milliVolts;
};

VoltsValue :: VoltsValue(const MilliVoltsValue & mV) : _volts(mV/1000.0f) {/* empty */}

class PowerMeter
{
public:
   PowerMeter() {/* empty */}

   void forceVoltage(const VoltsValue & v) {_voltsValue = v;}
   VoltsValue getVoltage() const {return _voltsValue;}

private:
   VoltsValue _voltsValue;
};

int main(int argc, char ** argv)
{
   PowerMeter meter;

   meter.forceVoltage(VoltsValue(5.0f));
   cout << "Current PowerMeter voltage is " << meter.getVoltage() << " volts!" << endl;

   meter.forceVoltage(MilliVoltsValue(2500.0f));
   cout << "Now PowerMeter voltage is " << meter.getVoltage() << " volts!" << endl;

   // The line below will give a compile error, because units aren't specified
   meter.forceVoltage(3.0f);   // error!

   return 0;
}
Carmellacarmelle answered 14/6, 2012 at 6:46 Comment(0)
L
2

I prefer avoiding macros where ever I can, and this is an example where it should be possible. One lightweight solution that gives you correct dimensions would be:

static double m = 1;
static double cm = 0.1;
static double mV = 0.001;

double distance = 10*m + 10*cm;

This also reflects the physical concept that units are something that's multiplied with the value.

Laughingstock answered 14/6, 2012 at 12:10 Comment(2)
Additional comment: You might want to use a namespace for this, and the user of your API can then decide if he prefers cluttering his namespace or doing a setVoltage(10 * CoolUnits::m). But still better than defining stuff.Laughingstock
Cute with little overhead, but its not enough. The problem with this is that $1m \neq 1$, but to $m$. That is, $1m = 100 cm$ but the unit themselves are also a term. This is an issue when a person tries to add a temperature to a distance. Your solution would allow it.Drumstick
C
1

Consider using an enum for your units and pass it as a second parameter:

namespace Units
{
    enum Voltage
    {
        millivolts = -3,
        volts = 0,
        kilovolts = 3
    };

    enum Time
    {
        microseconds = -6,
        milliseconds = -3,
        seconds = 0
    };
}

class PowerMeter
{
public:
    void forceVoltage(float baseValue, Units::Voltage unit)
    {
         float value = baseValue * std::pow(10, unit);
         std::cout << "Voltage forced to " << value << " Volts\n";
    }

    void settlingTime(float baseValue, Units::Time unit)
    {
         float value = baseValue * std::pow(10, unit);
         std::cout << "Settling time set to " << value << " seconds\n";
    }
}

int main()
{
    using namespace Units;
    PowerMeter meter;
    meter.settlingTime(1.2, seconds);
    meter.forceVoltage(666, kilovolts);
    meter.forceVoltage(3.4, milliseconds); // Compiler Error
}

Wrapping the Units namespace around the enums avoids polluting the global namespace with the unit names. Using enums in this way also enforces at compile time that the proper physical unit is passed to the member functions.

Calvinna answered 14/6, 2012 at 6:34 Comment(3)
Seeing as how the OP tried C++11 literals, enum class would be more appropriate.Yen
@chris: With enum class, there's no way to pull in all the unit names in the current scope with a using directive. So you'd have to prefix every unit (e.g. Voltage::volt).Calvinna
That's true. I guess it just depends on what kind of code the user is looking to write.Yen
T
1

I prefer the solution from Anders K, however you may use a template to save some time implementing all units as a separte class which can be timeconsuming and prone to errors as you may need to write a lot of code by hand:

enum Unit {
    MILI_VOLT = -3,
    VOLT = 0,
    KILO_VOLT = 3
};

class PowerMeter
{
public:

    template<int N>
    void ForceVoltage(double val)
    {
        std::cout << val * pow(10.0, N) << endl;
    };
};

Use like this:

        PowerMeter pm;
        pm.ForceVoltage<MILI_VOLT>(1);
        pm.ForceVoltage<VOLT>(1);
        pm.ForceVoltage<KILO_VOLT>(1);
Telethermometer answered 14/6, 2012 at 8:5 Comment(0)
U
1

Before you go crazy with anything more complicated, whenever you write new code that takes a quantity as an argument you should name your methods like this so that it's 100% clear:

PowerMeter.forceInMilliVolts( ... )
PowerMeter.settlingTimeInSeconds( ... )

And similarly use variables with the right names e.g.:

int seconds(10);
int milliVolts(100);

This way it does not matter if you have to convert, it is still clear what you are doing e.g.

PowerMeter.settlingTimeInSeconds( minutes*60 );

When you are ready with something more powerful move to that, if you really need to, but make sure you do not lose the clarity of which unit is being used.

Unskillful answered 14/6, 2012 at 15:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.