Copyright © 2001 by Michael S. Kenniston. For the most recent version check www.xnet.com/~msk/quantity. Permission is granted to use this document without restriction so long as this copyright notice appears in it.

When performing scientific or engineering calculations, one of the ways in which people have traditionally checked their work is to keep track of the dimensions of all intermediate results, and then verify that the answer has the proper dimensions. For example, if you are expecting a velocity but your answer comes out as a density, you know you've made a mistake somewhere. Unfortunately, few if any programming languages support this concept, so programmers almost always use raw numeric types (e.g. "double") to represent all the different quantities in a program.

The Quantity Library extends the C++ language to associate dimensions with each quantity variable or value, and it arranges for these dimensions to be checked for consistency at compile-time. There is no need for additional run-time space or time overhead, so ideally the final program should execute as fast as an equivalent program with no such checking.

To use the library, simply:

#include "cuj/quantity/quantity.hpp"If you want to do output, you must also:

#include "cuj/quantity/quantity_io.hpp"To use non-SI units and/or predefined physical constants:

#include "cuj/quantity/other_units.hpp" #include "cuj/quantity/physical_constants.hpp"

NIST SP811 describes the seven fundamental dimensions of the physical universe:

- length
- mass
- time interval
- electric current
- thermodynamic temperature
- quantity of substance
- luminous intensity

The library defines a template class that is used to specify these dimensions in the obvious way, so for example it includes the definition:

typedef dimensions< -3, 1, 0, 0, 0, 0, 0 > mass_density_d;(mass density is mass per volume). There is a long list of predefined dimensionalities, but if you need one that isn't there you can define your own in any application program.

Quantities, as defined in SP811, are simply values that include dimensions. For example, "3" is a number but "3 meters" is a quantity. The library allows you to declare variables that can hold quantities, but you must specify (at compile-time) the dimensions of the quantity. For example, a user program can declare:

quantity< mass_density_d > chamber_density;and then the compiler will verify that only values with compatible dimensions are ever assigned to that variable. All quantities have a default constructor, so you can create arrays or vectors of them. This default constructor leaves the value undefined, just as it would for a number.

There is a special case where all the dimensions are zero. The meaning is that such a value is just a pure number, so the library handles this case by using the primitive numeric type (e.g. "double") to hold such a value. (Instantiating the dimensions<> template class with all zeroes is prohibited.)

Once you have declared a quantity variable, you want to assign values to it. These values must be constructed with the seven fundamental SI units:

- meter
- kilogram
- second
- ampere
- kelvin
- mole
- candela

chamber_density = 3.458 * kilogram() / ( meter() * meter() * meter() );The units here are absolutely required; if you try to simply assign the number 3.458 the compiler will flag it as a fatal error. Unfortunately it will often produce the sort of verbose and obscure message typical of C++ template errors, but it will at least point you at the incorrect line. (The empty parentheses "()" are an artifact of C++; all predefined units and quantities are defined as inline functions to help the optimizer generate better object code.) There are square() and cube() functions defined on quantities, so the above can be stated somewhat more concisely as:

chamber_density = 3.458 * kilogram() / cube( meter() );

Defining every SI unit with every possible decimal prefix would lead to an explosion of identifiers, so only the primary units are defined (except that both "kilogram" and "gram" are defined). All the SI prefixes such as milli and mega are defined as pure numbers which may be used as multipliers:

quantity< energy_d > max_energy = 34 * mega() * joule();

The SI system includes "derived units" such as newton and watt. These are also defined by the library and can be used just like any of the base units. In addition, there is a separate header file called "other_units.hpp" which contains a long list of units which are not approved for use with SI (like foot, slug, and btu), but which you may need in your program to deal with legacy data or special requirements.

There is also a static member function zero() defined for the quantity of each dimensionality. This is provided for convenience, for example so you can compare a quantity to zero. Each different dimension has its own zero, because 0 * meter() is not the same as, and cannot be compared to, 0 * kilogram(). In fact:

if( current_power > 0 )is illegal; you must say:

if( current_power > quantity< power_d >::zero() )or

if( current_power > 0 * watt() )

The library defines the usual arithmetic operators +, -, *, and / on all legal combinations of quantities and numbers. Power and root functions are also defined. Specifically:

- Adding or subtracting two quantities of equal dimensions produces a result of the same dimensions. E.g. length + length = length.
- Multiplying or dividing any two quantities produces a resulting quantity which may be of different dimensions. E.g. length / time = speed. This rule allows you to mix numbers (all dimensions zero) with quantities, so number * mass = mass.
- Raising a quantity to an integer power produces the same result as repeated multiplication.
- Taking the integer root of a quantity is the inverse of raising to a power. If the dimensions of the result are not all integers, a compile-time error occurs. E.g. sqrt( area ) = length and nth_root< 3 >( volume ) = length, but taking sqrt( volume ) or nth_root< 3 >( area ) is an error.

The dimensions of the result of every operation are computed automatically by the library. The only time the user ever has to specify dimensions is when declaring a variable. The nth_power() and nth_root() functions are templated, because the compiler has to know which power or root you want in order to determine the dimensions of the result. For example:

quantity< volume_d > cube_volume; cube_volume = 3.456e+2 * cube( meter() ); quantity< length_d > cube_side; cube_side = nth_root< 3 >( cube_volume ); cout << "the side is " << cube_side << "\n";or, more concisely:

cout << "the side is " << nth_root< 3 >( 3.456e+2 * cube( meter() ) ) << "\n";

Exponential and trig functions are not defined on quantities.

The following table summarizes the legal and illegal operations.

*int*is an integer constant*num*is a number (int, long, float, double, etc.)*quan*is a quantity*quan1*and*quan2*are two quantities of different dimensions

Operation | Operand Type(s) | Result Type |
---|---|---|

Constructors | quan() quan( quan ) | quan |

quan( num ) num( quan ) quan1( quan2 ) | illegal | |

Assignment | quan = quan | quan & |

quan = num num = quan quan1 = quan2 | illegal | |

Addition Subtraction |
quan += quan quan -= quan | quan & |

quan += num quan -= num num += quan num -= quan quan1 += quan2 quan1 -= quan2 | illegal | |

+ quan - quan quan + quan quan - quan | quan | |

quan + num quan - num num + quan num - quan quan1 + quan2 quan1 - quan2 | illegal | |

Multiplication | quan *= num | quan & |

num *= quan quan *= quan quan1 *= quan2 | illegal | |

quan * num num * quan | quan | |

quan1 * quan1 | quan2 | |

quan1 * quan2 | If quan1 and quan2 are reciprocals: num | |

Otherwise: quan3 | ||

Division | quan /= num | quan & |

num /= quan quan /= quan quan1 /= quan2 | illegal | |

quan / num | quan | |

num / quan1 | quan2 | |

quan / quan | num | |

quan1 / quan2 | quan3 | |

Powers | nth_power< int >( quan1 ) square( quan1 ) cube( quan1 ) |
If int == 0: num |

If int == 1: quan1 | ||

Otherwise: quan2 | ||

Roots | nth_root< int >( quan1 ) sqrt( quan1 ) | When dimensions of quan1 are all even multiples of int: quan2 |

Otherwise: illegal | ||

Transcendental functions |
log( quan ), log10( quan ), etc. exp( quan ), pow( quan, num ), etc. sin( quan), cos( quan ), tan( quan ), etc. | illegal |

All I/O functions are defined in a separate header file called "quantity_io.hpp" so that you don't have to pull in <iostream> unless you really need to.

There is currently no input operator>> defined. To read a quantity, you must read the number and then multiply by the units, e.g.

double d; cin >> d; quantity< power_d > bulb_power = d * watt();You can't forget to multiply by the units, because if you do the dimensions won't match and you'll get a compile-time error.

There is an output operator<< defined, but it always formats quantities using just the seven base units. The output format is as close as you can get in plain ascii to the official notation described in NIST SP811, e.g.:

cout << 456789 * kilo() * watt() * hour() << "\n";produces:

1.64444e+012 m+2 kg s-2(the official notation would be "1.644 44 · 10

If you want to force output in particular units, simply divide by that unit (and print the unit yourself). For example,

cout << "One parsec is " << parsec() / mile() << " miles long.\n";or

quantity< energy_d > todays_consumption; //... const quantity< energy_d > kWhr( kilo() * watt() * hour() ); cout << "todays energy consumption was " << todays_consumption / kWhr << " kWhr \n";

Internally, each quantity is represented at run-time as one "double". If you wish to use "float" or "long double" instead, you can do so by defining CUJ_QUANTITY_REP_TYPE:

#define CUJ_QUANTITY_REP_TYPE long double #include "cuj/quantity/quantity.hpp"

In debug builds, calculations with quantities will typically run much more slowly than calculations with raw numbers. However, once you turn on the optimizer, the performance should be as good, or nearly as good, as that of raw numbers. Of course, this depends on the optimizer, and some do a measurably better job than others.

A primary design goal of this library was to run on as many compilers as possible, in spite of the fact that some of those compilers are not ISO-compliant and don't even implement the entire C++ language. This led to some peculiar implementation methods hidden inside the library, and but it had little effect on the interface so users should not notice anything out of the ordinary.

Although the library conveniently includes definitions of many units, it only checks dimensions. In most cases this is not an issue, since getting the units wrong typically means getting the dimensions wrong as well. However, there are cases where different units have the same dimensions; the most common is degrees (angle) and radians. You can do conversions in the usual way, like:

cout << "The sine of 90 degress is " << sin( 90 * degree_angle() ) << "\n";

but if you forget to multiply by degree_angle() the compiler won't catch it and you will erroneously get the sine of 90 radians instead.Under MSVC, even if you write a using statement referring to each name that you use, some of them may not work, e.g. quantity<>::zero tends to cause problems. You can use either of two workarounds: (1) say "using namespace cuj::quantity" to pull everything in, or (2) fully specify each name that causes a problem, e.g. "cuj::quantity::quantity<>::zero".

There are lots of things a units library could do that this one does not:

- As described above, the library does not catch errors in the use of units that have the same dimensions.
- You cannot have multiple representation types in the same program, i.e. you cannot have quantity< length_d, float > and quantity< length_d, long double >.
- You cannot have variables whose dimensions are determined or change at run-time. This means you cannot read arbitrary input into a variable.
- While you can add new units and dimensions, you can do so only by adding them to the source code and compiling them into your program. You cannot change the set of possible units at run-time.
- You cannot take fractional powers or transcendental functions of quantities, e.g. sqrt( meter() ) or exp( kilogram() ). Even if you could justify this mathematically, it would dramatically complicate the implementation.
- Conversion factors cannot change at runtime. For example, creating an eighth dimension of "currency" wouldn't be particularly useful, because the conversion rates between currencies would have to be fixed at compile-time.

Furthermore, if you need a more sophisticated library than this one, the "SIunits" library from Fermilab may suit your needs. It has more features than the Quantity Library, but it is not as portable; in particular it does not support MSVC. See http://www.fnal.gov/ for details.

NIST SP811 is the National Institute of Standards and Technology Special Publication 811. This, along with a great deal of other useful information about measurements and units, is available at their web site: http://www.nist.gov/.

Last updated: 2001-07-13