Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VisualC++

Units of measurement types in C++. Using compile time template programming.

4.95/5 (26 votes)
8 Dec 2014CPOL32 min read 79.7K   601  
A lightweight library allowing diverse unit types, seamless implicit scaling between them and the ability to work efficiently with multiple factor-less base unit systems (e.g. MKS and cgs).

This is the second release of this library - details of changes made can be found in the History section at the end. There are some fixes, a covenient way of defining a unit as the inverse of another has been provided and it now correctly handles datum based measurements such as Celcius or height above a datum.

Contents

#Introduction, #Background, #Features, #Datum Measurements#Quick reference,
#Encapsulation of dimensional analysis, #Architecture of a unit type
#Code changes in the second release#Final comments#History

Introduction

This article could be of interest to anyone looking for a self contained and usable units of measurement library but is also an illustration of how templates can be used to extend the work of the compiler in checking and interpreting the code you write without generating any extra run time code.

It is not opaque template meta programming but it makes light use of some of the techniques on which template meta programming is built. It is lightweight in that it generates no extra run time code but also in that it overburdens neither the compiler nor a programmer trying to understand how it works. I did not approach this with a mastery of template techniques, instead I have developed a particular working knowledge of them in order to solve particular problems. I believe this 'engineering' approach may be a good match for the world that many programmers live and think in.

It does not require any of the features extended by more recent versions of C++. constexpr would be usefull in this context but it remains far from universally available.

If you just want to use this library to get things done then go directly to the #Quick reference section and use the #Features section as a further reference.

Background

A variety of units of measurement libraries already exist but for some reason their use doesn't seem to be popular. To be honest I have only given them a quick glance because I don't really need a units library. I just wanted to write one, motivated by an interest in using compile time template techniques to produce richer more intelligent data types. Of course what I have written is the one that I would like to use should I need one but also with an eye on its potential use by others.

As with many units systems it has at its heart Barton’s and Nackman’s approach to dimensional analysis described by Scott Meyers at

http://se.ethz.ch/~meyer/publications/OTHERS/scott_meyers/dimensions.pdf

This is based on template parameters representing basic dimensions such as Length, Mass and Time.

C++
template<int Length, int Mass, int Time> class unit 
{       
double value;     
 ....
};   

so

C++
typedef  unit<1, 0, 0> LENGTH; 
typedef  unit<0, 1, 0> MASS; 
typedef  unit<0, 0, 1> TIME; 

and

C++
typedef  unit<1, 0, -1> VELOCITY;  // LENGTH/TIME = <1, 0, 0> - <0, 0, 1>
typedef  unit<1, 0, -2> ACCERATION;    // VELOCITY/TIME = <1, 0, -1> - <0, 0, 1> 
typedef  unit<1, 1, -2> FORCE;    // ACCERATION*MASS = <1, 0, -2> - <0, 1, 0> 

This makes it possible to check that assignment, comparison, addition and subtraction can only take place between units that have the same dimensions and that multiplication and division produce units of the correct type.

The most basic implementation of this approach has an elegant simplicity:

C++
LENGTH metres(1); 
LENGTH Kms(1000);  

LENGTH m = 5*metres; //m is implicitly metres and its value is 5
LENGTH k = 2*Kms; //k is implicitly metres and its value is 2000 

but it is slightly odd in its use of dimensions (e.g. LENGTH) as the data type (with implicit unstated units of metres) and variables as unit sizes. It is not directly units of measurement as the data type. Most units of measurement libraries provide a more sophisticated wrapping of this approach to overcome this and other limitations.

The following describes the features that have been built into this library.

Features

First of all to illustrate that this system doesn't suck you into any more complexity than your situation demands, if your only requirement is to work with length, area and volume then your units definition could be as simple as this:

C++
ulib_Dimension(1, LENGTH)
ulib_base_Unit(LENGTH, metres)

and this will enable you to write in your executable code:

C++
metres length=3; 
metres width=4; 
metres height=5; 
sq_metres area= length* width; 
cubic_metres volume=  area *  height;

You may also wish to use other units of length which you can define as scalars of the metres you have defined:

C++
ulib_scaled_Unit(Kms,  =, 1000   , metres)
ulib_scaled_Unit(cms,  =, 1/100  , metres)
ulib_scaled_Unit(feet, =, 0.3048 , metres)

and then in your executable code you can write;

C++
​metres m=1500;
Kms k=m;  //k will have a value of 1.5 (Kms) and m will have a value of 1500 (metres)
double ratio=k/m;    //ratio will have a scalar value of 1 = 1.5 (Kms)/1500 (metres)

The scaling between metres and Kms is automatic and implicit and follows the rule of conserving quantity. That is, if a variable declared as metres holding a value of 1500 is assigned directly to a variable declared as Kms then the Kms variable will be set to 1.5 (the same quantity). Conversion factors no longer need to appear in your code or even any indication that a conversion should be done.

Defining units

The work of defining units is carried out entirely with the macros that follow.

First of all Up to 7 dimensions can be named and used. There is a well established custom of using three and making them Length, Mass and Time. Others are possible but not so popular.

C++
ulib_Dimension(1, LENGTH)
ulib_Dimension(2, MASS)
ulib_Dimension(3, TIME)

these named dimensions can then be used to define base units. In this case we will use the MKS base units of metres, kilograms and seconds:

C++
ulib_base_Unit( LENGTH, metres)
ulib_base_Unit( MASS  , Kgs)
ulib_base_Unit( TIME  , secs)

we can add units scaled from these as required

C++
ulib_scaled_Unit( Kms,   =, 1000 , metres)
ulib_scaled_Unit( mins,  =, 60   , secs)
ulib_scaled_Unit( hours, =, 60   , mins)

and we can build up a range of compoud units derived from binary combinations of existing units by multiplication or division. Here are some common unscaled compound units

C++
ulib_compound_Unit( metres_per_sec,  =, metres,          Div,   secs)   //VELOCITY
ulib_compound_Unit( metres_per_sec2, =, metres_per_sec,  Div,   secs)   //ACCELERATION
ulib_compound_Unit( Newtons,         =, metres_per_sec2, Mult,  Kgs)    //FORCE
ulib_compound_Unit( Joules,          =, Newtons,         Mult,  metres) //ENERGY
ulib_compound_Unit( Watts,           =, Joules,          Div,   secs)   //POWER

we can also create compound units from scaled units

C++
ulib_compound_Unit( Kms_phour,       =, Kms,             Div,   hours)  //VELOCITY

and scaled units from compound units

C++
ulib_scaled_Unit( Kg_force, =, 9.806649999980076 , Newtons)

We can also define a unit as the inverse of another unit (added in 2nd release)

C++
ulib_Unit_as_inverse_of( Herz, secs)

Declaration

Once you have defined a unit, (say metres) you can declare variables using its name

C++
metres length;

its built in square and cubic forms

C++
sq_metres area;
cubic_metres volume;  

its inverse

C++
inverse_of<metres>::type closeness; 

or raised to any integral power

C++
to_power<metres, 4>::type variance_of_area; 

The ::type ending is an appropriate reminder that inverse_of and to_power are type modifiers for unit declarations (a new concept that requires a new expression). They do not perform any operation on numbers.. They do not do anything at run tme.

In all cases the operations available between and on units are:

Construction

  • from unit of same type
C++
metres m=metres(1500);
  • from unit with same dimensions
C++
metres m=Kms(1.5);
  • from a number
C++
metres m=1500; 

Assignment = , Addition and subtraction + - += -= , Less than and greater than comparision < >,

  • by unit of same type
C++
metres m = metres(1500);
m += metres(50);
if( m > metres(1350); 
     ;
  • by unit with same dimensions
C++
metres m = Kms(1.5);
m += Kms(0.050);
if( m > Kms(1.35); 
     ;

Multiplication and division * /

  • by any unit
C++
metres_psec V = metres(500) / secs(10); 
Joules Energy= Newtons(5) *  metres(10); 
  • by and of a number
C++
metres m = 2*metres(50)*5; 
metres m = metres(50) / 5;  
inverse_of<metres>::type closeness= 5 /  metres(50) ; 

Extraction of value as a double variable.as<unit_type>() or as<unit_type>(variable)

There is protection against inadvertant assignment of a unit variable with a numerical value in the wrong units and also against mistaking its units when reading its numerical value. The protection offered is line of sight. You can only assign a unit variable with a numerical value at the point of declaration where you can see the unit type you are dealing with.

C++
metres m = 5; //Ok  
.........
m = 15; //will not compile 
m = metres(15); //Ok  

and you can only extract the numerical value (type double) by using the variable.as<unit_type>() dot method or the as<unit_type>(variable) free function which require you to expicitly state the units in which you require the variable.

C++
double dist = m:          //will not compile - no implicit conversion
double dist_in_metres = m.as<metres>();     //ok
double dist_in_kilometres = as<Kms>(m);     //also ok, m will be converted to Kms
double mass_in_kilogrammes = as<Kgs>(m);   //will not compile - m is not a mass

Return unit variable representing positive integer root
variable.integer_root<root_function, positive integer>() 
or  integer_root<root_function, positive integer>(variable)

Returns a unit variable that is in the correct units for an integer root. Will only compile is the source unit type is a power which is a mltiple of the same  integer.
Requires function that performs the numerical root (takes a double, returns a double) to be passed as a template parameter

C++
sq_metres area =  1600;
metres side_of_square = area.integer_root<sqrt, 2>(); 

Test for aproximately equal according to given tolerance
variable1.is_approx<tolerance_unit, numerator, denominator>(variable2) .

Operator == (exactly equal) is not practically useful with measured quantities so it is not implemented. Instead a dot method testing for aproximately equal according to given tolerance is provided. The template arguments specify the units, numerator and denominator of the tolerace. The following are equivalent:

C++
if(m.is_approx<metres, 1, 100>(metres(1550))
;

if(m.is_approx<cms, 1, 1>(metres(1550))
;

if(m.is_approx<mms, 10, 1>(Kms(1.55))
;

Division with remainder - int n=variable1.goes_into(variable2, Remainder) 

This can be what is practically required in may circumstance. Units must match correctly including the Remainder variable passed in by reference to be filled.

Operators ++ and -- are not implemented because they have no meaning for a pure quantity. Their interpretation depends on the units chosen to measure that quantity. If you want their functionality you will have to write...

C++
metres m=5:
m+=metres(1);  //++
m-=metres(1);   //--

...which forces you to be explicit about the units involved in the change you are making to the quantity

The system has been designed to support expressions composed from these operations of any complexity.

 

The following global functions have been depreciated from the library for the reasons stated

  • Square root returning variable of appropriate unit type - Sqrt(variable)

Being a specific implementation of an existing function, integer_root<sqrt, 2>(variable) ,  that draws in an outside dependancy ( sqrt from math.h) it does not properly belong to the library and is depreciated to an external option.

  • <span style="font-size: 14.2857141494751px;">approx_equal<tolerance_unit, numerator, denominator>(variable1, variable2)</span>  - the global equivalent of the is_approx method
  • how_many_in(variable1, variable2, Remainder)  - the global equivalent of the goes_into method 

These global functions are generic beyond the scope of the library in that they will work correctly with just about any numerical data type.  This can useful but that same quality combined with their unmangled names carries a risk of clashing with other libraries. As such they cannot be implicit in the library and are depreciated to an external option.

These depreciated functions an be individually reactivated using the ulib_using_depreciated(function_name) macro

C++
ulib_using_depreciated(Sqrt) 
ulib_using_depreciated(approx_equal) 

 

Multiple factorless base unit systems (for example MKS and cgs)

If you have defined your base units as MKS (metres, Kilograms and seconds) and have defined centimetres as 1/100 metres and grams as 1/1000 Kilograms then any part of your code that is expressed in cgs units will calculate correctly and scale seamlessly. However it is annoying that there will be a hidden conversion to MKS units and back again for all intermediate calculations. For this reason this library will simultaneously respect more than one factorless base unit system (for example MKS and cgs) so that intermediate calculations can be factor free when dealing with metres kilogrammes and seconds and also when dealing with centimetres, grams and seconds. This is complicated by units (such as seconds in this case) that are common across factorless base unit systems but this problem has been solved for cases where the common units are common to all base systems in use (in practice it is quite difficult to contrive a situation where they would not be).

C++
ulib_Dimension(1, LENGTH)
ulib_Dimension(2, MASS)
ulib_Dimension(3, TIME)

ulib_multiple_Bases(2)

ulib_all_bases_Unit(TIME, secs)

ulib_base1_Unit(LENGTH, metres)
ulib_base1_Unit(MASS,   Kgs)

ulib_base2_Unit(LENGTH, cms,   =, 0.01  , metres)
ulib_base2_Unit(MASS,   grams, =, 0.001 , Kgs)

The MKS compound units (metres_per_sec etc.) can be now defined as before but units native to or with affinity to the cgs system can now be defined in relation to its base units:

C++
ulib_compound_Unit( cms_per_sec,  =, cms,          Div,  secs)   //Velocity
ulib_compound_Unit( cms_per_sec2, =, cms_per_sec,  Div,  secs)   //Acceleration
ulib_compound_Unit( Dynes,        =, cms_per_sec2, Mult, grams)  //Force
ulib_compound_Unit( Ergs,         =, Dynes,        Mult, cms)    //Energy  

Now you can declare diverse units and mix them in complex expressions. It will recognise which units are derived from which base system and adjust its working base system according to which predominates in each sub-expression:

C++
Newtons sum_forces = Newtons(4)+Kgs(1)*metres(2)/sq_secs(4)+Dynes(50)+grams(500)*cms(20)/sq_secs(3) ;

It will use MKS as its working base for the first part of this expression and cgs for the second part, performing all necessary conversions between the two. Remember, these decisions are made at compile time before you even execute the code. There is no dithering on ifs and else's at run time.

More dimensions and more base unit systems

Here is example that adds FPS(Feet, Pounds, seconds) to make three base unit systems and also a fourth dimension of ANGLE with units of radians across all three base unit systems. The dimension of ANGLE is used to qualify rotating systems so that torque does not have the same dimensions as energy. This is non standard (angle is considered to be a ratio of two lengths and therefore a dissolution of dimension) but if you put that ideological objection aside it works well and prevents confusion when you are working with rotating systems, as much engineering does.

The library has been coded to accept up to 7 dimensions and simultaneously respect up to 5 base unit systems.

C++
ulib_Dimension(1, LENGTH)
ulib_Dimension(2, MASS)
ulib_Dimension(3, TIME)
ulib_Dimension(4, ANGLE)

ulib_multiple_Bases(3)

ulib_all_bases_Unit(TIME,  secs)
ulib_all_bases_Unit(ANGLE, radians)

ulib_base1_Unit(LENGTH, metres)
ulib_base1_Unit(MASS,   Kgs)

ulib_base2_Unit(LENGTH, cms,    =, 0.01    , metres)
ulib_base2_Unit(MASS,   grams,  =, 0.001   , Kgs)

ulib_base3_Unit(LENGTH, feet,   =, 0.3048  , metres)
ulib_base3_Unit(MASS,   Pounds, =, 0.45359 , Kgs)

'Foot, Pound, Second' scaled and compound units can be built up in the same way as for MKS and cgs except that some unit names such as FPS_Force will need to be made up to represent unscaled compound units that don't have familiar names.

C++
ulib_compound_Unit( feet_per_sec,      =, feet,          Div,  secs)    //Velocity
ulib_compound_Unit( feet_per_sec2,     =, feet_per_sec,  Div,  secs)    //Acceration
ulib_compound_Unit( FPS_Force,         =, feet_per_sec2, Mult, Pounds)  //Force
ulib_compound_Unit( FPS_Energy,        =, FPS_Force,     Mult, feet)    //Energy

ulib_scaled_Unit( Pounds_Force, =, 3.217404867821228, FPS_Force)      //Force
ulib_compound_Unit( Feet_Pounds_Force, =, PoundsForce,   Mult, feet)    //Energy

Using ANGLE to break the apparent identity between Torque and Energy

We will use the more rational MKS system to look at how adding a dimension ANGLE with units of radians resolves the confusion (and scope for error) between energy and torque. The confusion arises because energy is defined as a the action of a force moving something over a distance in the direction in which it acts and torque is defined as the turning effect of a force applied at a distance from the center of rotation. The two distances play a different role. In the first it lies along the line of the force and is the result of it. In the second it is perpendicular to the force and determines where it acts. We need metres radius to be dimensionally distict from metres in the line of action.

To resolve thus, let us look at energy in a rotating system. In a rotating system, there is only work done (energy) if torque causes it to rotate through an angle.

Joules= Nm_torque * radians.

If we make ANGLE a dimesion with units of radians then we will get a dimensional distinction between Torque and Energy.

So rearranging the equation above:

Nm_torque = Joules/ radians .

Now if we call our radial distance radial_metres (name it and then find out what it is) then

Nm_torque = Newtons* radial_metres

equating Torque from the two equations above:

Newtons* radial_metres = Joules/ radians

and divide both sides by Newtons

radial_metres = metres / radians

This is the key. We use radial_metres to describe radial distances on rotating systems and we define it as a compound of metres divided by radians:

C++
ulib_compound_Unit( radial_metres, =, metres,  Div,  radians)

and then we can properly define Torque

C++
ulib_compound_Unit( Nm_torque,     =, Newtons, Mult, radial_metres)

Torque is now dimensionally distict from energy and has a coherent relationship with it.

C++
Joules WorkDone = Nm_torque (200)*radians(6.28);

This also provides a coherent relationship between angular displacement and the length of arc it makes at a radius.

C++
metres arclength = radial_metres(5)*radians(3.14);

Optimising scaling between units

Scaling between units is carried out using the scaling ratios that you have supplied. The manner of defining units ensures that it will always be possible to scale automatically between any two defined units but in some cases it may be carried out by a combination of those scaling ratios each time it is done. This will be the case between two units that are not base units and are not already defined as a ratio of each or are derived from different base systems. This is due to a limitation in how tradional compilers arrange the initialisation of const doubles.

Any of these more distant unit to unit scaling relationships may be collapsed to a single precalculated multiplication simply by providing an explicit relationship saying that it should be done.

C++
ulib_Precalc_unit_to_unit(Kms , mms) //forces pre-calculation at load time

Kms dist = mms(2500); //this is now performed with a single multiplication

The pre-calculation will not be carried out at compile time but it will be done once only at load time.

Datum based measurements

This section is a response to one of the comments this article recieved after its first publication pointing out that there is a need to provide an intercept besides a multiplication factor with coversion between some measurements such as from Celsius to Fahrenheit. What must be understood is that:

Celcius is measurement that uses degrees centigrade as its units.

There is no problem with defining degrees centigrade and degrees Fahrenheit as units in the normal way to represent temperature difference in the same way that we use seconds to represent time interval. Conversion between temperature differences requires only the application of a factor as is provided for. 

C++
ulib_Dimension(5,TEMPERATURE)
ulib_base_Unit(TEMPERATURE, degrees_C)
ulib_scaled_Unit(degrees_F, =, 0.5555555, degrees_C)

They can also be used to form compound units ....

C++
ulib_compound_Unit( degrees_C_per_metre,   =,  degrees_C,     Div,   metres) 
ulib_compound_Unit( degrees_C_psec,        =,  degrees_C,     Div,   secs) 

The problem with Celsius and temperature in Fahrenheit is that they are not properly measures of quantity or amount. They are points on reference scales each using a different physical datum as zero. This has two consequences:

  • That they require an offset to be applied when converting from one to another means that they must be declared as distinct from degrees centigrade and degrees Fahrenheit of temperature difference which only require the application of a factor during conversion.
  • That zero does not represent non-existence (no effect) means that it hardly ever makes sense to use them directly as factors in mathematical formulae. To illustrate the absurdity of doing so; whole terms would zero out just because you happen to hit the arbitrary zero datum value and would do so differently depending on if you used Celsius or Fahrenheit.

Nevertheless temperatures are the measurements you take and Celcius and Fahrenheit are common reference scales. Many formulae recieve temperatures as their inputs and calculate the temperature differences that they work with internally. So there is a real practical need to find a coherent way of embracing them.

This is it:

C++
ulib_datum_Measurement( Celcius, degrees_C, 0)
ulib_datum_Measurement( Fahrenheit, degrees_F, 32)

A datum measurement must be based on an existing unit and must specify its value at a physical datum common for all datum measurements of that dimension (in this case the freezing point of water).

Datum measurements are necessarily limited compared to normal quantity units. They are intended to store datum based measurements and correctly convert between measurements based on different datums and scales e.g. Celcius and Fahrenheit...

C++
Celcius temperature_C1=30;
Fahrenheit temperature_F1=temperature_C1;

...to be modifiable by adding or subtracting quantity units of the same dimensions:

C++
temperature_C1 += degrees_C(10);

...and to yeild a quantity unit as a result of the difference between two datum measurements...

C++
Celcius temperature_C2=50;
degrees_C temperature_difference_C = C2 - C1;

//or equally
temperature_difference_C = C2 - F1;
degreesF temperature_difference_F = C2 - F1;

Datum measurement types cannot be used to define compound units and datum measurements cannot be directly used as factors in mathematical formulae. There is also no conversion between a datum measurement and the quanitity unit on which it is based other than by differences as described above. 

The one exception that is not restrained by these limitations is an absolute datum measurement:

C++
ulib_absolute_datum_Measurement(Kelvin, degrees_C, 273)

Kelvin is an expression of temperature in the same way as Celcius and Fahrenheit but with a different datum. Therefore it must be defined as a datum measurement, especially if we want correct automatic conversion with Celcius and Fahrenheit. However its zero datum of absolute zero really does represent non-existence therefore its numerical value really does represent a true quantity and is used as such in mainstream thermodynamics. Accordingly an absolute datum measurement can be be used to form compound units...

C++
ulib_compound_Unit( Kelvin_Kgs, =,  Kgs,  Mult,   Kelvin)

...and absolute datum measurement variables can be directly used as factors in mathematical formulae if they are postfixed with ()

C++
kelvin abs_temp=200;
Kgs mass=5;

Kelvin_Kgs KKgs =abs_temp() * mass;

The new datum measurement types have been developed with some thought to their application in other dimensions. The library should embrace what it needs to in a generic manner. I can think of two other examples:

  • Date and Time - When we define seconds as a unit of the TIME dimension, we are really talking about a time interval not a point in time. If we want to store and use a Date Time then we must define it as a datum measurements with the Christian, Muslim, Chinese and computer manufactures versions all using different points in real time as their zero datum. N.B. Only cosmologists can even begin to talk about absolute time or the non-existence of time and although they may be quite certain about what happened in the first few seconds I don't think they can say with the same precision how long ago that was. So an absolute datum measurement in the time dimension isn't really viable.
  • Height (as in altitude) - It is sometimes convenient to record heights against a reference scale and enter them into a formula which will internally calculate the height differences that it works with. You may wish to record variously height above a particular point on the ground, height above sea level or height above the centre of the Earth. We can use datum measurement types for each of them which define how they are related and ensure that conversions are carried out correctly but height above the centre of the Earth has a case for being an absolute datum measurement. Zero above the centre of  the Earth really does represent absolute non-existence of height and it is the measurement that can be applied directly as a factor in geometrical formulae in the most general sense.  

 

Quick reference

The library (that is the engine for unit definitions) is made available for use simply by including a single header file (provided in the download) with no dependancies.

C++
#include "ulib.h"

Most of the library is wrapped in a private namespace that you need never know about. Your access to it is unseen and carried out by the macros used to define units. You may wish to wrap the units that you define in a namespace but if you do so, make sure that ulib.h is included within that namespace.

C++
namespace myunits{
#include "ulib.h"
#include "myunits.h"  //This is where you define your units
}

 

Macros used to define units (quantities) are:

C++
ulib_Dimension(1, LENGTH)

ulib_base_Unit(LENGTH, metres)

ulib_compound_Unit( metres_psec,    =, metres,            Div,    secs)

ulib_Unit_as_inverse_of( Herz, secs )

ulib_scaled_Unit(Kms,        =,    1000,        metres)

ulib_precalc_Unit_to_Unit(feet_pmin, Kms_psec)

and with multiple base unit systems:

C++
ulib_multiple_Bases(2)

ulib_all_bases_Unit(TIME, secs)

ulib_base2_Unit(LENGTH, cms, =, 0.01, metres)

//N.B. ulib_base_unit and ulib_base1_unit are equivalent.

Type modifiers for unit declarations are:

C++
sq_metres
cubic_metres
to_power<secs, 4>::type
inverse_of<secs>::type

Dot methods of unit type variables are:

C++
double variable.as<unit_type> ()
unit_type variable.integer_root<root_function, positive integer>()
bool variable.is_approx<tolerance_unit, numerator, denominator>(variable2)
int n=variable1.goes_into(variable2, Remainder) 

Named global functions are:

C++
double as<unit_type> (unit_variable)
root_unit_type integer_root<root_function, positive integer>(unit_variable)

Macros used to define datum measurements (reference points with respect to a datum) are:

C++
ulib_datum_Measurement(celcius, degrees_c, 0)

ulib_absolute_datum_Measurement(kelvin, degrees_c, 273)

Promotion of absolute datum measurement variable to normal quantity unit by postfixing with ()

C++
kelvin abs_temp=200;
Kgs mass=5;
//then
Kelvin_Kgs KKgs =abs_temp() * mass;
abs_temp() = KKgs / mass;
//also
KKgs = kelvin(200)()  * mass;

Global functions depreciated from the library are:

C++
root_unit_type Sqrt(unit_variable)
bool approx_equal<tolerance_unit, numerator, denominator>(numeric_variable1, numeric_variable2)
int how_many_in(U const& D, U2 const& N, U3 & R)

These can be activated with the ulib_using_depreciated(function_name) macro

Encapsulation of dimensional analysis

Barton’s and Nackman’s approach to dimensional analysis (mentioned earlier) is encapsulated by struct dimensions which has no data members because it is only involved in type formation and type matching at compile time.:

C++
template <int d1, int d2, int d3, int d4, int d5, int d6, int d7>
    struct dimensions 
    {
        enum {    D1=d1,    D2=d2,    D3=d3,    D4=d4,  D5=d5,  D6=d6,    D7=d7    };
    };

enum is used as a convenient way to declare a list of static const int members and initialise them. In this case they record the integer template parameters that define the class. As enums they are not allocated memory as variables and are simply inserted into runtime code wherever they appear, which in this case, is nowhere. As a result they act as pure compile time constants.

To work with this, structs are provided holding typedefs of dimensions structs representing operations on and combinations of other dimensions structs:

  • An explicit single dimension:
C++
template <int num> struct single_dimension
{
    typedef dimensions<(1==num)?1:0, (2==num)?1:0, (3==num)?1:0, 
    (4==num)?1:0, (5==num)?1:0, (6==num)?1:0, (7==num)?1:0> 
        dims;
}; 

//usage pattern
single_dimension<1>::dims LENGTH;
  • A power of an existing dimensions struct
C++
template<class dims_in, int P> struct dims_to_power
{
    typedef dimensions<dims_in::D1*P, dims_in::D2*P, dims_in::D3*P, 
            dims_in::D4*P, dims_in::D5*P, dims_in::D6*P, dims_in::D7*P> 
        dims;
};
  • Multiplication (op=Mult) and Division (op=Div) of one unit by another:
C++
//general definition
template<class t, class op, class b> struct Combine_dims;

struct Div{}; //Empty classes used only for selecting specialisation
struct Mult{};

//specialisation for multiplication - sum of dimension indices
template<class dims1, class dims2> struct Combine_dims<dims1, Mult, dims2>
{
    typedef dimensions<
        dims1::D1 + dims2::D1, dims1::D2 + dims2::D2, 
        dims1::D3 + dims2::D3, dims1::D4 + dims2::D4, 
        dims1::D5 + dims2::D5, dims1::D6 + dims2::D6, 
        dims1::D7 + dims2::D7> 
                    dims;
};

//specialisation for division - difference of dimension indices
template<class dims1, class dims2> struct Combine_dims<dims1, Div, dims2>
{
    typedef dimensions<
        dims1::D1 - dims2::D1, dims1::D2 - dims2::D2, 
        dims1::D3 - dims2::D3, dims1::D4 - dims2::D4, 
        dims1::D5 - dims2::D5, dims1::D6 - dims2::D6, 
        dims1::D7 - dims2::D7> 
                    dims;
};
  • Unit type of integer root of a unit type. A result that cannot be represented by integer powers of the base dimensions will produce a compile time error:
C++
template<class dims_in, int R> struct integer_root_of_dims
{
private:
    /*..................................................................
    struct div_exact  - Divides one integer by another 
    but produces an error if they don't divide exactly. 
    ..................................................................*/
    template<int D, int D2, int N> struct div_if_first_param_zero
    { private: enum{ res= "error_unit_is_not_raised_to_this_power"} ;  };

    template<int D, int N> struct div_if_first_param_zero<0, D, N>
    {    enum{ res= D/N};  };

    template<int D, int N> struct div_exact : 
        public div_if_first_param_zero<(D/N)*N - D, D, N> {};

public:
    typedef dimensions< div_exact<dims_in::D1, R>::res, 
                        div_exact<dims_in::D2, R>::res, 
                        div_exact<dims_in::D3, R>::res, 
                        div_exact<dims_in::D4, R>::res, 
                        div_exact<dims_in::D5, R>::res, 
                        div_exact<dims_in::D6, R>::res,
                        div_exact<dims_in::D7, R>::res > 
                dims;
};
 
These structs, that do nothing other than hold a typedef, provide the tools of dimensional analysis. This allows dimensions structs to be formed, operated on, combined and compared. Their existence and all the operations between them are exclusively compile time,
 

Architecture of a unit type

The basic architecture of a unit type is illustrated in a private macro called by the base unit definition macros:

C++
#define ULIB_BASE_UNIT(sys, dimension, name)                                            \
                                                                                        \
    struct  ULIB_DESC_CLASS(name) : public dimension                                    \
    {                                                                                   \
        enum { Scaled=ung::_UNSCALED, System=sys };                                     \
        template <int P> inline static double Base2This(double val)    {return val;}    \
        template <int P> inline static double This2Base(double val)    {return val;}    \
    };                                                                                  \
                                                                                        \
    typedef ung::named_unit<ULIB_DESC_CLASS(name),1> name;                              \
    typedef ung::named_unit<ULIB_DESC_CLASS(name),2> sq_##name;                         \
    typedef ung::named_unit<ULIB_DESC_CLASS(name),3> cubic_##name;
The ULIB_DESC_CLASS(name) macro simply adds an underscore to the beginning of the name you have passed in to be used as the name for a hidden description struct. The description struct inherits the dimensions struct you have passed in, sets enums to indicate if it is scaled and which base unit system it uses and provides two functions whose generic purpose is to convert any unit to its base unit and back. In this case we are defining a base unit so these functions simply return the value passed in - that is they have no effect. Even without the inline modifier, compilers are good at recognising such a simple case of no effect and the function will not even be called.
 
The unit name you passed in becomes a typedef of a <code>named_unit<> passed the description struct as a template parameter. In the case of metres this would expand as:
 
C++
#define ULIB_BASE_UNIT(0, LENGTH, metres)                                               \
                                                                                        \
    struct  _metres : public LENGTH                                                     \
    {                                                                                   \
        enum { Scaled=ung::_UNSCALED, System=0 };                                       \
        template <int P> inline static double Base2This(double val)    {return val;}    \
        template <int P> inline static double This2Base(double val)    {return val;}    \
    };                                                                                  \
                                                                                        \
    typedef ung::named_unit<_metres,1> metres;                                          \
    typedef ung::named_unit<_metres,2> sq_metres;                                       \
    typedef ung::named_unit<_metres,3> cubic_metres;
 
When you declare a unit type (metres) you are really declaring a named_unit<> class which is passed the units description class (_metres) as a template parameter. It is the named_unit<> template class that encapsulates the common behaviour of the unit types that you declare.
C++
template <
            class T, int P=1, 
            class dims=typename dims_to_power<T, P>::dims, 
            int Sys=T::System
            > 
    class named_unit
    {
 
The named_unit<> template class takes the unit description class T and the power P as template parameters and from them creates defaults for its template parameters dims (using the dims_to_power type modifier described above) and Sys. It is this class that determines whether an operation between units can be done and the dimensions of the resulting unit type. But the task of carrying out numerical calculations is largely delegated to specialisations of the structs: Unit2Unit<> and Base2Base<> which carry out any necessary conversions.
 
For example the following method allows construction of one named_unit from another
 
C++
template <class T1, int P1, int Sys2> //Construction
        inline named_unit::named_unit(named_unit<T1,P1, dims, Sys2> const& S)
            {val=Unit2Unit<T1, T, P1, P>::Convert(S.Value());} 
 
This will accept a named unit with any description class T1, raised to any power P1 and from any base unit system Sys2 but the dimensions template parameter must match its own dimensions template parameter dims. Having ensured that this operation can only be allowed if the units have matching dimensions, the actual assignment is carried out by Unit2Unit<T1, T, P1, P>::Convert(S.Value()). In many cases, for instance assignment by a unit of the same type, there is nothing for Unit2Unit<T1, T, P1, P>::Convert(S.Value()) to do and nothing will be done - that is no code will be generated to do anything. This will happen when the general form of Unit2Unit...
 
C++
template <class T1, class T2, int P1=1, int P2=1>
    struct Unit2Unit
    { 
        inline static double Convert(double val)    
        {
            return T2::Base2This<P2>
                (
                    Base2BaseByUnit<typename dims_to_power<T2, P2>::dims, T1, T2>
                    ::Conv
                    (
                        T1::This2Base<P1>(val)
                    )
                );
        }
    };
 
...encounters specialisations of This2Base, Base2Base and Base2This that do nothing. For instance operations bewteen base units, including unscaled compound units, of the same base unit system will find that the units' Base2This and This2Base methods will do nothing and that there also exists a specialisation of Base2Base for cases where both bases are the same, which also does nothing. The result is that it will be clear to the compiler that Convert has no effect so no code, not even a call, will be generated. The work of going through all this in order to decide that nothing needs to be compiled is of course done by the compiler - and it won't choke on it, there are no deep recursions.
 
In cases where conversions are indeed necessary the general form of Unit2Unit<> will only compile code for those parts of the conversion that are necessary. However in the case of conversion between compound units of different base unit systems, this can involve a string of multiplications.
 
The general form of Base2Base<> is:
 
C++
template <class dims, int Sys1, int Sys2> struct Base2Base
    {
    inline static double Conv(double val)
        {return 
                BaseShifter<dims::D1, 1, Sys1, Sys2>::Get(
                BaseShifter<dims::D2, 2, Sys1, Sys2>::Get(
                BaseShifter<dims::D3, 3, Sys1, Sys2>::Get(
                BaseShifter<dims::D4, 4, Sys1, Sys2>::Get(
                BaseShifter<dims::D5, 5, Sys1, Sys2>::Get(
                BaseShifter<dims::D6, 6, Sys1, Sys2>::Get(
                BaseShifter<dims::D7, 7, Sys1, Sys2>::Get(val)))))));}

    }

where the various specialisations of BaseShifter<> (one for each potential dimension) are provided as units are declared. A default specialisation is provided for all cases where the dimensions power dims::D1 etc. is zero:

C++
template <int Dimension, int Sys1, int Sys2>
    struct BaseShifter<0, Dimension, Sys1, Sys2>
    { 
        inline static double Get(double v){return v;}
    };

This ensures that the dimensions you haven't even named and dimensions not involved in the conversion automatically produce base shifters that have no effect.

Despite these efficiencies a conversion between scaled units of force from different base unit systems could involve four multiplications. For any two units with the same dimensions this may be reduced to one multiplication by using the macro ulib_precalc_Unit_to_Unit(unit1 , unit2)

C++
#define ulib_precalc_Unit_to_Unit(unit1, unit2)                                         \
                                                                                        \
    const double ULIB_UNIT2UNIT_RATIO(unit1, unit2)=                                    \
        ULIB_DESC_CLASS(unit2)::Base2This<1>(                                           \
            ung::Base2Base                                                              \
                <typename ung::dims_to_power<ULIB_DESC_CLASS(unit1), 1>::dims,          \
                ULIB_DESC_CLASS(unit1)::System, ULIB_DESC_CLASS(unit2)::System>         \
                    ::Conv(                                                             \
                        ULIB_DESC_CLASS(unit1)::This2Base<1>(1)                         \
                    )                                                                   \
                );                                                                      \
                                                                                        \
    template <int P> struct ung::Unit2Unit                                              \
        <ULIB_DESC_CLASS(unit1), ULIB_DESC_CLASS(unit2), P, P>                          \
    {                                                                                   \
        typedef ung::equal_dimensions                                                   \
            <ULIB_DESC_CLASS(unit1), ULIB_DESC_CLASS(unit2)> check_equal_dimensions;    \
                                                                                        \
        ULIB_DECLARE_POWX(ULIB_UNIT2UNIT_RATIO(name, unit2))                            \
        inline static double Convert(double val)    {return powX<P>()*val;}             \
    };                                                                                  \
    template <int P> struct ung::Unit2Unit                                              \
        <ULIB_DESC_CLASS(unit1), ULIB_DESC_CLASS(name), P, P>                           \
    {                                                                                   \
        ULIB_DECLARE_POWX(ULIB_UNIT2UNIT_RATIO(unit1, unit2It))                         \
        inline static double Convert(double val) {return (double)1/powX<P>()*val;}      \
    };
 

It starts by declaring a global const double that will be initialised at load time (not compile time) by making the same calls as Unit2Unit<> would. It cannot call Unit2Unit<> to do this because that would instantiate the general form for this combination and defeat the purpose. It then defines a specialisation of Unit2Unit<> for each direction between the two units that makes direct use of the precalculated global const double. Any conversions between these two units will then always find these specialisations and perform the conversion with a single operation.

 
Most operations on a named_unit<> will return a named_unit<> of the same type but multiplication or division by other units will return a unit type with different dimensions that may not correspond with any unit you have defined. This is particularly likely to happen with intermediate values in complex calculations. For this reason there needs to be a mechanism for dealing with unnamed unit types. This is provided by the template class x_unit<>.
 
C++
template <class dims, int Sys> class x_unit
    {
 
An x_unit<> does not have a name or unit description class. Its identity is determined only by its dimensions (dims) and its base unit system (Sys). Its units are always those of its own base unit system.
 
named_unit<> and x_unit<> are the only data structures involved in that they have a data member of type double holding the quantity they represent in their own units. They are the replacements for the raw doubles that would otherwise have been involved. Both have to provide all of the operations that can occur with those raw doubles. There are some differences in this area between named_unit<> and x_unit<>: named_unit<>s are declared in your code. They are variables you can do things with so they need to support +=, -= and assignment =. Conversely x_unit<>s are not just unnamed types they are also temporary variables that have no name with which you can work. You do not declare them, they are created automatically and invisibly as intermediate steps in evaluating expressions that you have written. They do not need to support +=, -= and assignment = because as unnamed temporaries it is impossible for those operations to be called.

Now we can look at what happens when you divide one unit type variable by another, lets say metres by seconds.

C++
metres dist=10;

secs time=5

metres_per_sec velocity = dist / time;

dist is of type metres which is a typedef of named_unit<_metres, 1> and time is of type secs , typedef of named_unit<_secs, 1>  so the operator / of named_unit<> that takes another named_unit<> will be called:

C++
template <class T1, int P1, class dims2>  // /
inline x_unit<
        typename Combine_dims<dims, Div, dims2>::dims, 
        SysFilter<Sys,T1::System>::sys
        > 
    named_unit::operator / ( named_unit<T1, P1, dims2> b) const 
        {
            return UnitByUnit<T,T1,P,P1>::Divide(Value(), b.Value());
        }

This method will take a named_unit<> of any name, to any power and from any base unit system. This is represented by the introduction of new template arguments...

C++
template <class T1, int P1, class dims2>

...and their unqualified application to completely fulfill the type of the value argument

C++
operator / ( named_unit<T1, P1, dims2> b) const  
The unit type of the return value is formed by the Combine_dims structure described above in the encapsulation
C++
typename Combine_dims<dims, Div, dims2>::dims 
and the SysFilter struct that determines the appropriate base unit system for the return value (the operation of this is described further on).
C++
SysFilter<Sys,T1::System>::sys
The actual numerical division is carried out by struct UnitByUnit<>
C++
template <class T1, class T2, int P1=1, int P2=1>
struct UnitByUnit
{
private:
    inline static double OtherToThisBase(double val)
        {
        return Base2BaseByUnit<typename dims_to_power<T2, P2>::dims, T1, T2>
                ::Conv
                (
                    T2::This2Base<P2>(val)
                );
        }
public:    
    inline static double Divide(double v1, double v2)    
        {return T1::This2Base<P1>(v1) / OtherToThisBase(v2);}

    inline static double Multiply(double v1, double v2)
        {return T1::This2Base<P1>(v1) * OtherToThisBase(v2);}
};
This ensures that all conversions are carried out to put both operands in base units of the left operand's base unit system. In this case, both operands are in base units of the same base unit system so due to collapsing of inline functions that have no effect, the call to OtherToThisBase will disappear in the compilation and only v1/v2 will be compiled.
 
The return value of the operator / overload will be an x_unit<>
C++
x_unit<
        typename Combine_dims<dims, Div, dims2>::dims, 
        SysFilter<Sys,T1::System>::sys
        > 
So what happens next with this x_unit<>? It may be invisibly combined with other x_unit<>s or named_unit<>s to form further x_unit<>s but eventually you will have to capture the result as a named_unit<> to be able to do anything with it and this is where you have to get your units right as we do when we write:
C++
metres_per_sec velocity = dist / time; 
velocity is a named_unit<> which has been passed _metres_per_sec as a description class and it is being constructed from the result of dist / time which will be an x_unit<>.
 
named_unit<> has a constructor taking an x_unit<> as an argument:
C++
template<int Sys2> inline named_unit::named_unit(x_unit<dims, Sys2> const& S)
{
    val=ConvertFromXUnit<Sys2>(S.Value());
}
This constructor will take an x_unit<> of any base unit system (new template parameter Sys2) but it must have the same dimensions struct as the named_unit<> (template parameter dims of the named_unit<>). If we had written...
C++
Newtons force = dist / time;   //error will not compile 
...then we would get a compile error telling us that this is wrong, the x_unit<> produced by dist / time does not have the same dimensions as Newtons.
 
The value of the named_unit is set by a call to the private member ConvertFromXUnit...
C++
template<int Sys2>    inline static double ConvertFromXUnit(double val)
{
    return T::Base2This<P>(
        Base2BaseBySys<dims, Sys2, Sys>::Conv(val)
                );
}
...which does any conversions necessary. In this case the metres_per_sec unit is already in base units so ConvertFromXUnit will do nothing and the constructor will compile to:
C++
template<int Sys2> inline named_unit::named_unit(x_unit<dims, Sys2> const& S)
{
    val=S.Value();
}

If you are working with a single base unit system then all System template parameters such as Sys2 above will always be zero and they will play no real part in the action. However if you are working with multiple base unit systems then it is the parameter that distinguishes between them. At first sight it may seem enough that each base unit system is represented by a distinct number but what number do you use to represent units such as seconds which are typically shared by more than one base unit system (e.g. MKS and CGS)? The solution I chose works as follows:

Each base unit system has an internal number used for the int System template parameter. Each is an integer power of 2 so that each base unit system is represented by a binary bit. We indicate that a unit is agnostic across base unit systems by intialising its System enum with a logical OR of the base unit systems to which it belongs.

We need a way of determining which base unit system should be used for the temporaries (x_units<>) in which calculation continues and we need to recognise that the ORed values (representing units such as seconds) are accepted by all the base unit systems as being of thier own. The is done by template struct SysFilter<>:

C++
template<int Sys1, int Sys2> struct SysFilter 
{
private:
    template<int SysANDSys2, int Sys> struct SysFilterReturn    //general case
    { enum {sys=SysANDSys2}; };

    template<int Sys> struct SysFilterReturn<0, Sys>            //specialisation when 1st param is 0
    { enum {sys=Sys}; };
public:
    enum {sys = SysFilterReturn<Sys1 & Sys2, Sys1>::sys};    
};

SysFilter is passed two int Sys template parameters. If the logical AND of them is non zero then they are either the same or one of them is a combined bases symbol (2 or more bits set, as used for example by seconds), that matches the other base system. In this case there will be no change of base system the logical AND ensures that only one bit is propagated. However if the logical AND evaluates as zero then there is a change of base unit system and the first int Sys template parameter is used unmodified as the new working base. In the case that both Sys template parameters are combined bases symbols such as seconds*seconds, there will be a propagation of the 2 bits of the combined bases symbol but it doesn't matter, one of those bits will be removed as soon as it encounters any unit that is not base system agnostic. SysFilter<> initialises its sys enum using struct SysFilterReturn<> in order to pass it the Sys1 & Sys2, Sys1 construction. SysFilterReturn<>, in turn uses template specialisation to provide an appropriately initialised sys enum. Its usage is SysFilter<Sys1, Sys2>::sys.

SysFilter<> is used wherever two base unit system numerical codes (System enum) need to be compared or combined. Much of its use is encapsulated in the Base2BaseBySys<> & Base2BaseByUnit<> template structs which are used as convenience wrappers for base to base conversions.

C++
/******************************************************************        
These wrap the use of Base2Base<> providing it with base system parameters that have been processed by 
SysFilter<>. Two versions are provided for convenience in different calling contexts.
*******************************************************************/

template <class dims, int Sys1, int Sys2> struct Base2BaseBySys 
: public Base2Base
<dims, SysFilter<Sys1,Sys2>::sys, 
SysFilter<Sys2,Sys1>::sys>
{};
template <class dims, class T1, class T2> struct Base2BaseByUnit 
: public Base2Base
<dims, SysFilter<T1::System,T2::System>::sys, 
SysFilter<T2::System,T1::System>::sys>
{}; 

 

Code changes in the second release

Greatly assisted by the attention and diligence of others I have made some code changes for the second release.

1. The application of some fairly straightforward partial specialisation to deal generically with self to self conversions rather than generate them one by one with macros. These include

Self to self for Unit2Unit - which fixed a run-time inefficiency when adding units of the same type 

C++
template <typename T, int P>
struct Unit2Unit<T, T, P, P>
{
      inline static double Convert(double val)
       {
        return val;
       }
};

Self to self for Base2Base - which fixed an unintended requirement to include ulib_multiple_Bases(1) when only using one base system. 

C++
template <class dims, int Sys> struct ung::Base2Base<dims, Sys, Sys>                        
{
    inline static double Conv(double v)
    {
        return v;
    }
};

and any base to any base for the specific dimension of an all bases unit - which replaced a lot of macro generation.

C++
#define ulib_all_bases_Unit(dimension, unit)            \
                            \
    ULIB_BASE_UNIT(ulib_OredBases, dimension, unit)    \
    template <int P, int Sys1, int Sys2 >             \
    struct ung::BaseShifter<P, dimension::Num, Sys1, Sys2>    \
        { inline static double Get(double v){return v;} };    \
    template <int Sys1, int Sys2>                \
    struct ung::BaseShifter<0, dimension::Num, Sys1, Sys2>    \
        { inline static double Get(double v){return v;} };    

2. The provision of a new unit definition macro allowing a unit to be defined as the inverse of another

C++
ulib_Unit_as_inverse_of(name, orig)    

This is an adaptation of ulib_compound_Unit(name, is, t, Operation, b) with the first term replaced by a scalar (zero dimensions).

3. Review and rationalisation of dot methods and free functions. 

The as<>() function, designed to force you to specify units as a template when extracting the numerical value from a unit type...

C++
/*************************************************************************************
as<>() - function to release the raw double from a unit
**************************************************************************************/
template<class U> 
inline double const& as(U const& nu)
    {return ung::_as<U>(nu);}

...suffers from two faults:

  • You don't have to specify a template parameter because the compiler can deduce it.
  • ​It will accept almost anything as an argument and with such a short name it could clash with other libraries

The following replacement solves both problems. The compiler will only consider arguments that are x_units and named_units (there is also one for datum_units) and a correct unit type template must be supplied. 

C++
/****************************************************************************
as<>() - function to release the raw double from a x_unit
*****************************************************************************/
template <class U, class dims, int Sys> 
inline double const& as(ung::x_unit<dims, Sys> const& nu)
    {return nu.as<U>();}

/***************************************************************************
as<>() - function to release the raw double from a named_unit
****************************************************************************/
template<class U, class T, int P> 
inline double const& as(ung::named_unit<T, P> const& nu)
    {return nu.as<U>();}

as , integer_root, is_approx and goes_into are now available as dot methods of named_units and x_units (datum_units only support the as and is_approx method).

The Sqrt, approx_equal and how_many_in global functions have been depreciated for reasons of library integrity.

4. Addition of datum measurements data type.

This is represented in code by the datum_unit class template. It is similar to the named_unit class template but supports more limited functionality. 

C++
template <
    class D, //description class of datum_unit
    class T, //description class of named_unit on which it is based
    class dims=typename dims_to_power<T, 1>::dims, 
    int Sys=T::System
    > 
class datum_unit

Datum measurement types are defined using the ulib_datum_Measurement(Celcius, degrees_C, 0) macro:

C++
#define ulib_datum_Measurement(name, orig, offset)                \
struct ULIB_DESC_CLASS(name)                        \
{                                    \
    enum {absolute=0};                            \
    static double DoOffset(double val){return val+(offset);}            \
    static double UndoOffset(double val){return val-(offset);}            \
};                                    \                                                            \
typedef ung::datum_unit<ULIB_DESC_CLASS(name), ULIB_DESC_CLASS(orig)> name;    

...which defines a description cass for the datum measurement providing offset information and passes that and  the description class of the named_unit on which it is based, to a datum_unit.

The datum_unit class template uses the datum measurement description class to calculate offsets and the named_unit description class to calculate dimensional suitability.

Absolute datum measurement types are defined using the ulib_absolute_datum_Measurement(Celcius, degrees_C, 0) macro:

C++
#define ulib_absolute_datum_Measurement(name, orig, offset)                \
struct ULIB_DESC_CLASS(name) : public ULIB_DESC_CLASS(orig)            \
{                                    \
    enum {absolute=1};                            \
    static double DoOffset(double val){return val+(offset);}            \
    static double UndoOffset(double val){return val-(offset);}            \
};                                    \
typedef ung::datum_unit<ULIB_DESC_CLASS(name), ULIB_DESC_CLASS(orig)> name;

This has two differences:

The datum measurement description class inherits from the named_unit description class. This gives it a set of dimensions that allows the absolute datum measurement to be used to form compound units.

The absolute enum is set to 1. This enables an operator()() in the datum_unit class

C++
/**********************************************************************
If absolute, can be read as a named_unit by appending ()
***********************************************************************/
typename if_true_use_type_else_unknown
    <
        D::absolute,
        named_unit<T, 1, dims, Sys>
    > ::result_type &    operator()()
{
    return reinterpret_cast<named_unit<T, 1, dims, Sys>& >(*this);
}

that returns a reference cast as a named_unit. The if_true_use_type_else_unknown class template defines result_type as a named_unit<T, 1, dims, Sys> if D::absolute is true or as an unknown class if if D::absolute is false.

Final comments

It might look like there is a lot of code wrapped around each unit type but it is all compile time book keeping. All that gets compiled is doubles and the combinations of them that you write in your code - just as if all units had been declard as doubles. The only extra code generated is the automatic scaling between units, which you would otherwise have had to write explicitly.

To clarify the compile time nature of this library: Only data requires run-time storage and only functions and methods that do something will generate run-time code. Structs and classes that only consist of enums and typedefs and functions and methods that are called but have no effect have no runtime presence whatsoever. If you use scaled units then memory will be allocated at runtime to store all of the conversion factors involved and there will be an execution of their initialisation at load time. The application of those scale factors is of course code that would have to exist anyway.

To fully understand how everything works together you will have to look at the source code. It is quite readable and well commented.

History

First release - September 2014

Second release - December 2014

  • Fix- Unintended requirement to include ulib_multiple_Bases(1) when using only one base unit system. 
  • Fix- Unintended run time inefficiency when adding units of the same type. 
  • Fix- No easy way of defining a unit as an inverse of another.  A new ulib_Unit_as_inverse_of( Herz, secs)  unit definition macro is now available for this.
  • Fix- Was possible to use the as<>() free function without supplying a unit type as a template parameter. 
  • Fixas<>() is now also available as a dot method of unit type variables.
  • Fix - Global functions that bring dependancies or that could clash with other libraries depreciated.
  • Enhancement - Addition of datum measurements for measurements such as temperature in Celsius and Farenheight whose numerical value does not represent a quantity or amount but rather a point on a reference scale with an arbritrary datum as zero. 
    Comments on code changes in the second release are collected here.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)