User Guide for units::scalar Class Library

Introduction

A scalar is a one-dimensional real number that represents a single measured value, such as length, time, or mass. No reference is assumed for the measured value, and a negative measured value is just as valid as a positive one. The application determines the context of the measurement and the validity of it.

Typically, software is written with implicit units for measurements. That is, a double or float variable is declared and assigned a number which is implied to be in a particular unit of measure. For example:

      double length = 5;  // meters
This practice can be very dangerous because unit mismatches cannot be detected at compile time. For instance, if a function is declared to take a length in feet, we could accidentally pass it meters and not be aware of the error until run-time. Even worse, we could just as easily pass a completely different unit of measure. For instance, we could pass 10 seconds to a function expecting a length in meters.

The basic idea of the scalar class library is to abstract the idea of a unit of measure so that C++ code can be written without the pitfalls of implicit units. By declaring base classes for all the common types of measure and subclasses for common units for each of these types, we can use the compiler to detect unit mismatches. The strong type checking of C++ makes it much more difficult (but not impossible) to accidentally mix units.


Construction

Great effort was taken to minimize the overhead associated with using the scalar classes. Since operating on measured values is much more common than construction and initialization, all scalar classes are designed to have a fixed internal representation. This internal representation is hidden from the application so that it can change without affecting anything else. Because of this, a scalar class takes up the same memory as a double value.

Units must be specified with any value assigned to a measurement variable. For example,

      Length length = 5;                 // error
      Length length = MetersLength(5);   // OK
Default constructors are provided for abstract unit types so that a value can be assigned later. The default value is undefined (just as with a double) because assuming a default would introduce unwarrented overhead. Example:
      Length length;

      length = MetersLength(5);

Once a unit of measure is created, it's identity will prevent it from being used when a function is expecting different units. For example:

      extern void doSomething(Length &length);

      doSomething(SecondsTime(5));    // error
      doSomething(MetersLength(5));   // OK
      doSomething(FeetLength(5));     // OK

You should not be concerned about construction overhead associated with constant values. For instance:

      if (angle < DegreesAngle(45))
      {
      }
Some application programmers might be concerned that the DegreesAngle constructor will incur unwanted conversion overhead every time this test is performed. They will instead attempt to use the internal units to avoid the conversion:
      if (angle < RadiansAngle(0.78539816))
      {
      }
For one thing, this is a dangerous assumption since the internal representation is hidden and could potentially change. Further, since most people don't think in radians, this makes the code difficult to read and understand. The first method should be used without concern because most compilers will optimize constant arithmetic out so no overhead is incurred at run-time.


Comparison

Testing for equality can be done in two ways: binary and similar. Binary equality is the same as testing double == double. This is potentially dangerous because it compares the binary images of the two numbers, not their values. The slightest variation can lead to an inequality. It's up to the application to decide if this is the desired behavior.

More often, the application will wish to test if two measurements are "close enough" to each other. This can be done with the similar function provided with all unit classes. Each unit class has a default tolerance for determining how close two values need to be to be considered similar. The value of the default tolerance can be accessed via a const data member. For example:

      if (similar(MetersLength(2), MetersLength(3)))
      {
         // This will never be true.
      }

      if (similar(MetersLength(2), MetersLength(2.5), MetersLength(1)))
      {
         // This is true.
      }

      // Test for similarity with much tighter tolerance than default.
      if (similar(a, b, MetersLength::defaultTolerance * 0.1))
      {
      }

Inequality operators are provided for all base unit types. You can mix subclasses of the same base to make things readable. For example:

      if (MetersLength(1) > FeetLength(1))
      {
         // This is true.
      }
      if (SecondsTime(1) < HoursTime(1))
      {
         // This is true.
      }
Inequality operators do not use tolerances.


Arithmetic

Once a unit of measure has been constructed, it can be manipulated in many ways just like a double variable. The application developer should look at the appropriate header file to see the full list of valid operators. Some examples are:

      Length a = MetersLength(5);

      Length b = 2 * a / 3.4;

      b += 1.2;

      Length c = (a - b);
Note that the unit type is preserved in arithmetic. That means you can mix different units of the same type:
      Length a = (MetersLength(5) + FeetLength(2));   // OK
      Length a = (MetersLength(5) + SecondsTime(2));  // error
Most units will cancel themselves by division. In other words, the ratio of two Lengths is a double:
      Length a = MetersLength(5);
      Length b = MetersLength(5);

      double ratio = a/b;


Cross-Unit Arithmetic

Some unit classes have arithmetic operators defined so that they can interact with other units. These interactions are the common relationships between units:

Speed = Length / Time
Acceleration = Speed / Time
Force = Mass * Acceleration
AngularSpeed = Angle / Time
Length = Angle * Length
Area = Length * Length
Volume = Area * Length
Density = Mass / Volume
MassFlowRate = Mass / Time
Frequency = double / Time
Pressure = Force / Area

These relationships can be rearranged to suit the application. For instance:

      // Compute speed.
      Speed speed = MetersLength(5) / SecondsTime(2);

      // Compute distance traveled after a duration.
      Length distance = speed * SecondsTime(10); 

      // Compute time to travel a distance.
      Time time = speed / FeetLength(23);


Unit Conversion

Because the internal representation is fixed, conversion between units take place in the constructor. Most applications should then deal only with the abstract types. However, there are times when an application must get a double representing the current value. For instance, if an equation cannot easily be converted to use the abstract types, the measurement must be converted to the units the equation was expecting.

For example, the Standard Atmosphere model uses the equation:

      static const double thermalLapseRate = 0.006506986;  // deg C / meter

      static const Temperature seaLevelTemperature = CelsiusTemperature(15.0);

      Temperature computeTemperature(const Length &mslAltitude)
      {
         return (seaLevelTemperature -
                 thermalLapseRate * mslAltitude);
      }
This won't compile because you can't subtract a Length from a Temperature. Since there is no unit class defined for Temperature divided by Length, the application must specify the units to be used for the multiplication. Fortunately, there is a double cast operator defined for all unit subclasses. This can be used any time the units are known:
      double meters = (double)MetersLength(5);
But what about when you only have an abstract measurement? That's when you must convert the measurement into the desired units.

There are two ways of converting a unit of measure: using the copy constructor and typecasting a reference. For example, the previous example should have been written like this:

      Temperature computeTemperature(const Length &mslAltitude)
      {
         return (seaLevelTemperature -
                 CelsiusTemperature(thermalLapseRate *
                                    (double)(MetersLength &)mslAltitude);
      }
Typecasting can only work if you have a reference to a non-temporary object. Trying to typecast the result of a function or operation will usually fail to compile. In these cases, you should use the copy constructor.


Output Formatting

There are times when the application must present a value to the user. Since the user needs to see the value in a particular units, the units must be specified by the application. An ostream operator is provided for all subclasses which prints the value followed by the units abbreviation. For example, the following prints "2.3 ft":

      cout << FeetLength(2.3) << endl;

For some unit types, there are additional formats which are not available as subclasses. For instance, Speed has several common combinations of Length and Time types, but certainly not all of them. In these cases, you can use the format template classes. For instance, if we wanted to print a speed in miles per minute, we would use the following:

      Speed speed = KnotsSpeed(400);
      cout << SpeedFormat< StatuteMilesLength,
                           MinutesTime >(speed) << endl;

Note that the following will print slightly different things:

      Speed speed = KnotsSpeed(400);
      cout << SpeedFormat< NauticalMilesLength,
                           HoursTime >(speed) << endl;

      Speed speed = KnotsSpeed(400);
      cout << (KnotsSpeed &)speed << endl;
The first will print "400 nm/hr", while the second will print "400 kts".

The format template classes also provide a double cast operator which allows the application to get the value in whatever units are required.


Special Types

While most of the unit types are fairly straight-forward in usage, there are a few notable types which warrent special consideration: Temperature and Angle.

Temperature

Since temperature unit conversions involve an additive offset (for instance, Kelvin = Celsius + 273), the following arithmetic operations are not defined for the abstract class: scaling, negation, and ratio. Doubling a Kelvin value gives quite a different temperature than doubling a Celsius value! You must specify which units to be operated on and then convert back to the desired units. For instance:

      Temperature a = CelsiusTemperature(23);
      Temperature b = a*2;  // error
      Temperature b = CelsiusTemperature((double)(CelsiusTemperature &)a)*2);

Angle

Mathematically, there are two types of angles: flat and circular. A flat angle can have any value and has no concept of wrapping around. In other words, with a flat angle, 0 != 360 degrees. This is useful in representing how far a shaft has rotated, for instance. The Angle class is a flat angle and gives unconstrained representation of a measure of angle.

A circular angle is one where values wrap around; 0 == 360 degrees. There are two popular references for circular angles: one that goes from -180 to 180 degrees and one that goes from 0 to 360 degrees. These are the SignedAngle and UnsignedAngle classes, respectively. Both will wrap when testing for similarity. For instance:

      if (similar(SignedDegreesAngle( 179),
                  SignedDegreesAngle(-179), DegreesAngle(3))
      {
         // This is true.
      }
      if (similar(UnsignedDegreesAngle(  1),
                  UnsignedDegreesAngle(359), DegreesAngle(3))
      {
         // This is true.
      }
Subtracting two circular angles will always return the smallest angle between the two angles. For instance:
      (SignedDegreesAngle(-170) -
       SignedDegreesAngle( 170)   == DegreesAngle( 20)

      (SignedDegreesAngle( 170) -
       SignedDegreesAngle(-170))  == DegreesAngle(-20)

      (UnsignedDegreesAngle( 10) -
       UnsignedDegreesAngle(350)   == DegreesAngle( 20)

      (UnsignedDegreesAngle(350) -
       UnsignedDegreesAngle( 10)   == DegreesAngle(-20)

All angle classes can be used with trig functions. For instance:

      cos(DegreesAngle( 90)) ==  0
      sin(DegreesAngle(-90)) == -1
      tan(DegreesAngle( 45)) ==  1
All angle classes can be multiplied by a Length radius to get the arc-length:
      DegreesAngle(90) * MetersLength(1)



List of Currently Supported Units