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:
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.
double length = 5; // 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 a template class that keeps track of the exponents of all basic units of measure, 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. Template arithmetic operators keep track of exponent manipulation so that proper units of measure are enforced even after complex arithmetic combinations of units of measure.
The main scalal class is the Unit
class template that
encodes the exponents of the fundamental SI units of measure (mass,
length, time, current, temperature, amount, intensity, angle):
However, using this class can be unwieldy, so some convenience
typedefs for double values are provided (Length, Time, Speed, etc.).
The full list of scalar typedefs is here.
You can be more explicit that you want a float or double valued type
by prefixing the type name with "f" or "d" (e.g.
template < typename ValueType_,
int massExp_,
int lengthExp_,
int timeExp_,
int currentExp_,
int temperatureExp_,
int amountExp_,
int intensityExp_,
int angleExp_ >
class Unit;
fLength
or dLength
).
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, the scalar Unit class template is 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,
A default constructor is provided 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 = 5; // error
Length length = MetersLength(5); // OK
Some convenience special types are provided for representing zero,
positive infinity, or negative infinity in any unit of measure:
Length length; // UNINITIALIZED!
length = MetersLength(5);
Length length1 = zero; // equivalent to: = MetersLength(0)
Length length2 = infinity;
Length length3 = negInfinity;
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:
Some application programmers might be concerned that the
if (angle < DegreesAngle(45))
{
}
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:
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.
if (angle < RadiansAngle(0.78539816))
{
}
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. This uses a tolerance for determining
how close two values need to be to be considered similar. For example:
if (similar(MetersLength(2), MetersLength(3), MetersLength(0.1)))
{
// Will never get executed.
}
if (similar(MetersLength(2), MetersLength(2.5), MetersLength(1)))
{
// This is true.
}
Inequality operators are provided. You can mix subclasses of the same
base unit of measure to make things readable. For example:
Inequality operators do not use tolerances.
if (MetersLength(1) > FeetLength(1))
{
// This is true.
}
if (SecondsTime(1) < HoursTime(1))
{
// This is true.
}
Once a unit of measure has been constructed, it can be manipulated in many
ways just like a double variable. Some examples are:
Note that the unit type is preserved in arithmetic. That means you can mix
different specific units of the same base type:
Length a = MetersLength(5);
Length b = 2 * a / 3.4;
b += FeetLength(1.2); // OK
b += 1.2; // error
Length c = (a - b);
Most units will cancel themselves by division. In other words, the
ratio of two Lengths is a double:
Length a = (MetersLength(5) + FeetLength(2)); // OK
Length a = (MetersLength(5) + SecondsTime(2)); // error
Length a = MetersLength(5);
Length b = MetersLength(5);
double ratio = a/b;
The Unit clas template defines all arithmetic operators so that variables with different units of measure can be arbitrarily combined in a type-safe manner. So for instance,
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);
You can declare a variable with an arbitrary unit of measure by using
the Unit class template:
typedef Unit< double, -1, 3, -2, 0, 0, 0, 0, 0 > GravitationalConstantUnits;
// Gravitational constant
GravitationalConstantUnits G = MetersVolume(6.67300e-11)/(KilogramsMass(1)*SecondsTime(1)*SecondsTime(1));
typedef Unit< double, 0, -1, 0, 0, 1, 0, 0, 0 > TemperaturePerLength;
TemperaturePerLength thermalLapseRate = CelsiusTemperature(0.006506986)/MetersLength(1);
Because the internal representation is fixed, conversion between units
take place in the constructor. Most applications should then deal
only with the base types. However, there are times when an
application must get a double representing the current value. For
instance, you might need to pass a double value to a third-party
library. To do this, use the value()
member function on
a specific unit instance:
void wrapper(Length const & length)
{
double meters = MetersLength(length).value();
call_function(meters);
}
The following functions are provided for any unit of measure type:
So for instance,
sqrt
abs
min
max
sqrt(FeetArea(16)) == FeetLength(4)
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:
The first will print
Speed speed = KnotsSpeed(400);
cout << SpeedFormat< NauticalMilesLength,
HoursTime >(speed) << endl;
Speed speed = KnotsSpeed(400);
cout << KnotsSpeed(speed) << endl;
"400 nm/hr"
, while the second will
print "400 kts"
.
For arbitrary units of measure, you can use the Format class template
which allows you to specify the specific fundamental units of
measure. Or more typically, you would use one of the two
typedefs: MksFormat
for Meters-Kilograms-Seconds
or CgsFormat
for Centimeters-Grams-Seconds. For example,
this prints "6.673e-11 m^3/kg-s^2".
cout << MksFormat(G) << endl;
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.
There are two types of temperatures: relative and absolute. The
generic Temperature class is relative and can be used in arbitrary
arithmetic and combined with other units of measure (i.e.,
Temperature/Voltage). AbsTemperature has an offset (for instance,
Kelvin = Celsius + 273) and therefore cannot be scaled or combined
with other units of measure. So the following arithmetic operations
are not defined for AbsTemperature: scaling, negation, and ratio.
Doubling a Kelvin value gives quite a different temperature than
doubling a Celsius value! You can use relative and absolute
temperatures together like this:
AbsTemperature a = AbsCelsiusTemperature(23);
AbsTemperature b = a*2; // error
Temperature deltaT = CelsiusTemperature(4.5);
deltaT *= 2; // OK
AbsTemperature b = a + deltaT;
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:
Subtracting two circular angles will always return the smallest angle
between the two angles. For instance:
if (similar(SignedDegreesAngle( 179),
SignedDegreesAngle(-179), DegreesAngle(3))
{
// This is true.
}
if (similar(UnsignedDegreesAngle( 1),
UnsignedDegreesAngle(359), DegreesAngle(3))
{
// This is true.
}
(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:
All angle classes can be multiplied by a
cos(DegreesAngle( 90)) == 0
sin(DegreesAngle(-90)) == -1
tan(DegreesAngle( 45)) == 1
arccos( 0) == DegreesAngle( 90)
arcsin(-1) == DegreesAngle(-90)
arccos( 1) == DegreesAngle( 45)
Length
radius to get
the arc-length:
(DegreesAngle(180) * MetersLength(1)) == MetersLength(M_PI)