Click here to Skip to main content
15,881,803 members
Articles / Programming Languages / C++11

Units and measures for C++ 11

Rate me:
Please Sign up or sign in to vote.
4.95/5 (22 votes)
24 May 2017CPOL53 min read 45.1K   919   18   8
Type quantities according to the units in which they are measured. A complete implementation of units of measurement as a data type for C++ 11.
Presented here is a library providing units of measurement as a data type for C++11 (specifically the compiler provided with Visual Studio Express 2015). It is quite simple to use and is for application anywhere where you are storing, retrieving, comparing or calculating with measured quantities. It is all contained within a single header that references just from the standard library. It is not a library of units. It is a code engine that allows you to define and use whatever units you want.

Image 1Introduction, Background,
Overview,

Using the code,
The example application,
Quick Reference, How it worksHistory

Introduction

In September 2014 I published Units of Measurement types in C++ on The Code Project which embraces the use units of measurement directly as a data type e.g.:

C++
metres d(50);
secs t (2);
metres_psec v = d / t;
  • N.B. This is different to the more traditional approach exemplified by boost::units which has a less direct syntax and some conceptual discomforts (see Background below):

    C++
    LENGTH d = 50*metres;	//metres has been defined as LENGTH * 1
    TIME t = 2*secs;		//secs has been defined as TIME * 1
    VELOCITY v = d / t;

It also allows scaled units that measure the same thing to be used seamlessly in any context and ensures correct results by implicitly carrying out any required conversions:

C++
Kms d(50);
mins t (2);
miles_phour v = d / t;

but is careful not to compile any conversion code (not even 'times one') when all arguments are unscaled members of the same rational unit system (e.g. metres, Kgs, secs) and will do this for multiple rational unit systems (e.g. MKS and cgs).

Unit definitions follow a protocol which is a close match to our textbook understanding of how units relate to each other.

C++
ulib_Base_unit(metres, LENGTH)
ulib_Base_unit(Kgs, MASS)
ulib_Base_unit(secs, TIME)
ulib_Scaled_unit(Kms, =, 1000, metres)
ulib_Scaled_unit(mins, =, 60, secs)
ulib_Scaled_unit(hours, =, 60, mins)
ulib_Compound_unit(metres_psec, =, metres, Divide, secs)//VELOCITY
ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)//ACCELERATION
ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)//FORCE
ulib_Compound_unit(Joules, =, Newtons, Multiply, metres)//ENERGY
ulib_Compound_unit(Watts, =, Joules, Divide, secs)//POWER

The new version presented here for C++ 11 builds on this using new features of the language. Initially the intention was simply to exploit the constexpr keyword to resolve an outstanding run-time performance issue resulting from the lack of support for floating point numbers as compile time constants in pre C++11 compilers. However one thing led to another and it has been entirely rewritten because I found C++ 11 so much more capable of expressing my design intentions. Most of the work has gone into ensuring that it is robust, truly generic, compiles optimal code and is simple and seamless to use. However there are some new features that are more visible:

  • It now provides literal suffixes for each unit that you define so you can have literal unit typed constant quantities e.g. 2_Kms and 4_mins. It is just syntactic sugar but it is nice and it is a perfect fit.

  • Writing for C++11 also brings a new obligation of constexpr correctness to which it fully conforms

    C++
    constexpr metres dist = 200_metres; //constant quantity
    constexpr secs t = 2_secs;          //constant quantity
    constexpr metres_psec velocity = dist / t; //constexpr expression
  • It provides an optional tolerance operator ^ for use with comparison and equality tests

    C++
    if(length1 == length2 ^ 0.1_metres)  //if equal within 0.1 metres
        do_something();
    This makes equality tests more practical for measured quantities and other comparisons more precise in their meaning.
  • As well as ensuring that conversions are optimal it also ensures that they are not carried out unnecessarily, so

    C++
    sq_metres Area = 3.14159 *  2000_metres * 2000_metres;
    will be evaluated in metres to produce a result in sq_metres, and
    C++
    sq_Kms Area = 3.14159 *  2_Kms * 2_Kms;
    
    will be evaluated in Kms to produce a result in sq_Kms without any conversions to intermediate units.

    However if you can loose this flexibility if you encapsulate the expression as a function taking a specified unit type.

    C++
    sq_metres area_of_circle(metres radius)
    {
    	return 3.14159 *  radius * radius;
    }
    Although you can still call it passing Kms or cms as well as metres, it will handle it by forcing a conversion to metres. To avoid these unwanted conversions you can now define generic functions that specify abstract dimensions of measurement as arguments rather than specific units.
    C++
    template<class T>
    auto area_of_circle(LENGTH::unit<T> radius) //radius can be any unit that measures LENGTH
    {
    	return 3.14159 *  radius * radius;
    }
  • For those cases where diversely scaled units are brought together in a product with more than two arguments a variadic product function is provided.

    C++
    mins time_taken2 = product_of<mins>(2_Kms_pHour, 3_grams , divide_by, 4_PoundsForce);
    which will ensure a compile time consolidation of all of the diverse conversions involved into a single factor for the entire line of code. Writing it conventionally as a chain of binary operators
    C++
    mins time_taken = 2_Kms_pHour * 3_grams / 4_PoundsForce;
    will result in consolidation of conversions for each of the three binary operators =, * and / which is not the same thing. The result, of course, will be equally correct..
  • In support of unit type correctness at the user interface it provides:

    A function that can be called to return the name of any unit as a text string. This enables the unit type to be systematically displayed alongside its value.

    A function that can be called to prepare a combo box to offer a list of all compatible units which the user can select for display units.

Background

The need to type quantities arises when you are dealing with different types of quantities or the same kind of quantity measured in different units. You want the type system to prevent inappropriate operations and permit appropriate ones ensuring that they produce correct results. It is not difficult to see how you can create a classes metres and secs and arrange for them to enforce the following:

C++
metres dist = metres(10); //ok
metres dist = secs(10); //error will not compile

but if you want it to properly embrace more intelligent use of units then you also have to enforce:

C++
metres_psec velocity = metres(10) / secs(2);  //ok
metres_psec velocity = metres(10) * secs(2); //error will not compile

You could spend a long time coding up a class for each unit complete with all operations that it can make with other units. You would find yourself forever chasing your tail, inventing and coding up unfamiliar units to cover all the combinations that might occur. Fortunately there is a generic approach to doing this systematically.

To be able to check the correctness of units in combination, their types need to have some complexity and rules need to be defined for combination and comparison of them. As with many units system this one has at its heart Barton 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 powers of basic dimensions such as Length, Mass and Time.

C++
template<int Length, int Mass, int Time> class unit 
{    
    enum {length=Length, mass=Mass, time= Time};   
    double value;     //the data member
 ....
}; 

so we can define the fundametal dimensions of measurement

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

and compound  dimensions of measurement built from them

C++
using  VELOCITY = unit<1, 0, -1>;  // LENGTH/TIME = <1, 0, 0> - <0, 0, 1>
using  ACCERATION = unit<1, 0, -2>;    // VELOCITY/TIME = <1, 0, -1> - <0, 0, 1> 
using  FORCE = unit<1, 1, -2>;    // 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 traditional approach , as exemplified by boost::units uses these dimensions of measurement as the data type and defines units as quantities scaled from those dimensions. So units are defined as follows:

C++
LENGTH metres(1); 	
LENGTH Kms(1000);
TIME secs(1);
TIME hours(3600);

and you can write

C++
LENGTH total_length = 50 * metres + 3 * Kms;
VELOCITY v = 50 * Kms / (5 * hours);

This has an elegant simplicity but it also has an uncomfortable conceptual twist in that by declaring metres to be a LENGTH of 1 you are fixing LENGTH to mean metres, which is fine but you still write LENGTH. You can't write metres because metres isn't a type, it is a quantity. Although it will handle scaled units typed into an expression it is otherwise not well adapted to the use of scaled units. You can't receive a result in a scaled units such as Kms, because Kms is a quantity, not a type. If you add together a string of quantities in Kms then they will effectively be converted to one by one to LENGTH (which is metres remember) before being summed. It is a little odd, has no mechanism to propagate scaled units and is inflexible about the units it uses to evaluate expressions.

I was puzzled by why we could not have plain units as the data type and write things more naturally. So I set out to do it myself and find out.

I quickly found out that there was a fundamental difficulty in implementing it. If you have scaled units such as Kms (= 1000 metres) as a type, then that type has to carry a conversion factor which in the general case is a floating point number and the language (pre C++11) did not allow that. In fact it didn't allow floating point numbers to be compile time constants at all. Even a global const double did not exist during compilation, only instructions to create and initialise it at run-time.

I really wanted to see this more natural syntax in action and emboldened by my experience that anything can be done with C++ if you take the trouble, I persisted and in September 2014 published Units of Measurement types in C++ on The Code Project.

Yes, anything can be done with C++ (pre 11) except pre-calculating compound conversion factors during compilation. This meant that some of the implicit conversions would be compiled as a chain of factors that would be re-calculated on each call. You could apply a bit of diligence and specify that you wanted certain conversions pre-calculated during program startup but I could not arrange for it to just be done automatically.

Units libraries are supposed to be zero overhead and compile as if it had all been written using a built in numerical data type. One that inserts implicit conversions can only claim this if it inserts no more code than would otherwise have to be written by hand in the most diligently optimal manner. On this criteria it had failed and meeting that criteria became the imperative for the rewrite for C++11 presented here.

It is likely that this approach has not been followed before because it was impossible to do it properly. With C++ 11 it can be done properly and the two great enablers are:

  • constexpr which enables floating point numbers as compile time constants and type information (static const class members)

  • the replacement of typedef with using = and the much needed template<> using = which reduces the complexity of the code to a level where you can design effectively.

This is what I present here.

Overview

The basic principle is :

Define some units that you want to use:

C++
#include "ulib_4_11.h"
ulib_Base_dimension1(LENGTH)
ulib_Base_dimension2(MASS)
ulib_Base_dimension3(TIME)

ulib_Begin_unit_definitions

	ulib_Base_unit(metres, LENGTH)
	ulib_Base_unit(secs, TIME)
	
	ulib_Unit_as_square_of(sq_metres, metres)
	
	ulib_Scaled_unit(Kms, =, 1000, metres)
 	ulib_Scaled_unit(mins, =, 60, secs)
 	ulib_Scaled_unit(hours, =, 60, mins)
		
	ulib_Compound_unit(metres_psec, =, metres, Divide, secs)
 	ulib_Compound_unit(Kms_phour, =, Kms, Divide, hours)

ulib_End_unit_definitions

Write some code that uses them:

C++
metres width = 10_metres;        //the required width of a road 
sq_metres area = 20000_sq_metres;    //the amount of tarmac available, expressed as coverage
    
Kms length =  area / width;        //the length of road that can be built
secs transit_time = 40_secs;    //time it takes to comfortably drive a car the length

Kms_phour velocity = length / transit_time; //safe velocity in familiar units

The first thing to notice is that every quantity is typed by the units it is measured in. The units chosen here are the ones which you would normally use to measure and talk about the quantities involved. So the width of the road is expressed in metres but the length in Kms. Also velocity is expressed as Kms_phour but the transit time used to calculate it is expressed in secs. Yet the code shows no conversion factors or functions to deal with this. The magic here is that the necessary conversions are implied by the unit types involved and are calculated and inserted into the code during compilation.

You will also see that it accepts Kms_phour as valid for accepting the result of length / transit_time whereas if it had been length * transit_time it would have rejected it and refused to compile it. This is of course what you expect of any units of measurements library; that it allows all of the correct combinations possible but rejects any attempt to mix incompatible units. This one though is not phased by the expression requiring several conversions to be carried out. Each operator (in this case / and =) will identify and fold together the required conversions during compilation and insert just a single factor into the compiled code.

More complex units of measurement are defined as binary combinations of existing ones. The following builds definitions for rational MKS mechanical units:

en-GB
ulib_Compound_unit(metres_psec, =, metres, Divide, secs)//VELOCITY
ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)//ACCELERATION
ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)//FORCE
ulib_Compound_unit(Joules, =, Newtons, Multiply, metres)//ENERGY
ulib_Compound_unit(Watts, =, Joules, Divide, secs)//POWER

Defining units

You begin by defining some fundamental dimensions of measurement

C++
ulib_Base_dimension1(LENGTH)
ulib_Base_dimension2(MASS)
ulib_Base_dimension3(TIME)

Here you see the familiar LENGTH, MASS, TIME because it is the classic and most universally understood approach. There are other unfamiliar but coherent and self consistent approaches and they are supported equally.

The first units you must define are some base units that represent these dimensions

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

and by doing so you are defining the primary base or rational unit system – in this case MKS (metres, Kgs, secs). All other units will be defined (directly or indirectly) with reference to these base units:

C++
ulib_Unit_as_square_of(sq_metres, metres) 	//rational
ulib_Unit_as_cube_of(cubic_metres, metres)	//rational

ulib_Scaled_unit(Kms, =, 1000, metres) 	//scaled
ulib_Scaled_unit(mins, =, 60, secs)	//scaled
ulib_Scaled_unit(hours, =, 60, mins)	//scaled

ulib_Unit_as_inverse_of(Herz, secs)		//rational

ulib_Compound_unit(metres_psec, =, metres, Divide, secs)		//rational
ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)	//rational
ulib_Compound_unit(Kms_psec, =, Kms, Divide, secs)		//scaled

The base or rational unit system also determines the working units of evaluation wherever quantities with diverse units types are combined through multiplication or division. This guarantees, as would be expected, that expressions and sub-expressions whose arguments are all unscaled rational units will be evaluated without conversions and without the intrusion of any conversion code.

Units are rational if there is no scaling in their reference to base units. Among the above, only Kms, mins, hours and Kms_psec are not rational units (Kms_psec because it is defined with reference to Kms which is scaled). These are handled safely and seamlessly but at the cost of implicit conversions.

Multiple rational unit systems

MKS isn't the only established rational unit system. There is also cgs (centimetres, grams, seconds) and there could be some sections of your code that are better expressed in these units or even hard-wired to them at a low level by physical constants in the code or high speed instrumentation readings.

Although you could define cms and grams as a scaled units and get perfectly correct results, there will be unwanted conversion thrashing going on as expression evaluations convert them to MKS rational units and your cgs declarations convert them back. Instead, you can register cgs as a secondary rational unit system

C++
ulib_Secondary_rational_unit_systems(cgs)

and declare cms and grams as base units of it

C++
ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)
ulib_Secondary_base_unit(cgs, grams, =, 0.001, Kgs)

secs is already defined as a base unit (MKS) so it is adopted.

C++
ulib_Secondary_base_adopt_base_unit(cgs, secs)

This is not strictly necessary because, by default, primary base units will be adopted for any given dimension unless a secondary base unit or explicit adoption is defined and secs is the primary base unit for TIME.  

Define some cgs mechanical units with reference to them

C++
ulib_Compound_unit(cms_psec, =, cms, Divide, secs)
ulib_Compound_unit(cms_pmin, =, cms, Divide, mins)
ulib_Compound_unit(cms_psec2, =, cms_psec, Divide, secs)
ulib_Compound_unit(Dynes, =, cms_psec2, Multiply, grams)
ulib_Compound_unit(Ergs, =, Dynes, Multiply, cms)

and now, the following expression will evaluate without conversions

C++
cms_psec v = 30_cms / 5_secs;

The compiler detects that cgs is the best fit rational unit system for cms and secs and therefore evaluates a result without conversions in cms_psec which then requires no conversions to your declared variable.

Multiple rational unit systems are not just about being able to conform to more than one convention. You can invent them strategically to suit your requirements. For instance, Kms and mins are probably the units that the mind can most easily grasp for aerial navigation. ( 8 Kms over the next minute gives you are more instant picture than 480Kms/hour or 134 metres/sec). You can define a rational unit system and call it KmsMins

C++
ulib_Secondary_rational_unit_systems(cgs, KmsMins)

and then define Kms and mins as members of it

C++
ulib_Secondary_base_unit(KmsMins, Kms, =, 1000, metres)
ulib_Secondary_base_unit(KmsMins, mins, =, 60, secs)

allow it to adopt Kgs

C++
ulib_Secondary_base_adopt_base_unit(KmsMins, Kgs)

and define a rational unit of velocity to go with it

C++
ulib_Compound_unit(Kms_pmin, =, Kms, Divide, mins)

Now you can declare your variables as Kms, mins and Kms_pmin and enjoy factor free evaluation when combining them e.g.

C++
Kms_pmin v = 40_Kms / 5_mins; //evaluates without conversions

You will also be able to use this preferred choice of units throughout your code without invoking conversions. Having familiar units at all levels of your code can be very helpful when debugging. Implausible and critical corner case values can be spotted far more easily.

You can define as many rational unit systems as you find useful. Each of them enjoying factor free evaluations. There is no problem with their coexistence. They can even meet in the same expression:

C++
Newtons(4)+Kgs(1)*metres(2)/secs::squared(4)+Dynes(50)+grams(500)*cms(20)/secs::squared(3) ;

The entire right hand side of the expression will be evaluated without conversions (all cgs) and yeild an intermediate result in the cgs unit of Dynes. The left hand side will also be evaluated without conversions (all MKS) and carries out just one conversion as it adds the intermediate result of the right hand side in Dynes to produce a final result in the MKS unit of Newtons.

Declarations and initialisation

Once you have defined a unit you can declare quantities in that unit:

C++
metres len;

You can intialise it by explicitly passing a numerical value in the constructor

C++
metres len(4.5); //len set to 4.5 metres

but otherwidse it can only be initialised or assigned by a compatible unit typed quantity (one that measures the same thing)

C++
metres len = 4.5_metres; //len set to 4.5 metres
len = 1.2_Kms;	//len set to 1200 metres

This ensures that you cannot enter a numerical value into a unit type without seeing the name of the unit type (in this case metres) written close to it on the same line of code. Specifically you cannot write

C++
len = 1200; //ERROR

which lacks a visible indication of the type of len and therefore would invite errors.

Similarly you can only extract the numerical value from a unit type by calling the as_num_in<unit_type>(quantity) function which requires you to explicitly state the units in which you wish to recieve it. It will convert to your chosen unit if it is compatible (measures the same thing) and will produce a compiler error if not.

C++
metres length = 200_metres;

double len_in_metres = as_num_in<metres>(length);   // ok - len_in_metres is set to 200
double len_in_Kms = as_num_in<Kms>(length);         // ok - len_in_Kms is set to 0.2

double try_this = as_num_in<secs>(length);          //ERROR - incompatible units

There are some type modifiers that can be applied to the units you have defined as follows:

C++
metres::squared area;
metres::cubed volume;
metres::to_power_of<4> variance_of_area;
sq_metres::int_root<2>  side_of_square;  //gives an error if unit can not be rooted.

They are useful for anecdotal use of powered units. If you frequently talk about a unit type in a powered form e.g. sq_metres then it is better to formally define it as you are accustomed to seeing it, hence:

C++
ulib_Unit_as_square_of(sq_metres, metres)
ulib_Unit_as_cube_of(cubic_metres, metres) 

Also with sq_metres and cubic_metres established as formal types it becomes easier to express temporary mathematical transformations of them e.g.

C++
cubic_metres::squared variance_of_volume;

This clarifies that the measured quantity is a volume and it is squared because it is a variance. It conveys more information than metres::to_power_of<6>.

Expressions and operators

Expressions are tied together by operators and unit typed quantities support all of the standard arithmetic and comparison operators and do exactly what you would expect with them. The following operations are supported;

=, +, -, +=, -=%,  %= with any unit type that can be used to measure the same quantity. The return type will be that of the left hand argument.

<, >, <=, >=, ==, !=, with any unit type that can be used to measure the same quantity. The return type will be a bool.

*,with any unit type. The return type will be a new temporary unnamed rational compound unit representing the combination. The rational unit system chosen (e.g. MKS or cgs ) will be that of the left hand argument.

*, where the type of both arguments are powers of the same unit type. The return type will be the same unit type raised to the sum or difference of the two powers.

*, /, *=, /= with a raw number. The return type will be the unit type.
with a raw number as the left hand argument. The return type will be the unit type.
with a raw number as the left hand argument. The return type will be the inverse of the unit type.

The return types from these operations determine the working units in which evaluation of the expression will proceed. When faced with combining different unit types through multiplication and division it will resort to rational units but otherwise will avoid converting away from the units you have chosen to use.

This means that a sum of lengths expressed in Kms

C++
Kms total_length = 2_Kms + 3* 5_Kms + 4_Kms;

or a comparison

C++
bool is_bigger = 5_Kms > 3_Kms;

will be evaluated entirely in Kms without any conversions and so will this

C++
sq_Kms Area = 3.14159 * 5_Kms* 5_Kms;

but the following

C++
Kms dist_travelled = 5_ Kms_phour * 2_hours;

will evaluate 5_ Kms_phour * 2_hours in rational units and the result will then need to be converted to Kms. The rational unit system chosen (if you are using more than one) is that of the left hand argument and the conversion to it is carried out by a single factor.

The tendency towards unscaled rational working units will be strongly felt because so many expressions do involve combining different unit types to create new compound units. However having those that don't require this stay with the units you have supplied will avoid a lot of unnecessary conversions when working with scaled units.

Operations that would make no sense with typed quantities are not suported. For instance ++ and -- cannot have a meaning that is independant of the units in which it is expressed, therefore they are not supported. Neither would bitwise operators. In fact bitwise operators are so alien to the situation that one of them ^ has been given a completely different meaning in this context.

The tolerance operator ^

operator == returns true when two numbers are exactly equal. That is a bit of hit or miss thing (mostly miss) with floating point numbers. So much so that it is only practically useful for seeing if a variable has been disturbed in any way. If you are going to talk about measured quantities being equal then you have specify within what tolerance. The same applies to operator != and also operators <= and >= are barely distinguishable from < andif you don't specify a tolerance.

For this reason a tolerance operator is available that can follow and qualify any of the comparison operators <, >, <=, >=, ==, != as follows:

C++
if( distance1 == distance2 ^ 5_cms) // if distance1 equals distance2 within a tolerance of 5cms.
    do_something();

The choice of ^ as operator is not arbritray. It has a lower precedence than the comparison operators <, >, <=, >=, ==, != and a higher precedence than the boolean logic operators && and ||. This is so that it brings no requirement to use brackets within conditional expressions.

C++
if (m == m2 ^ 1_metres || m == m2 ^ 1_metres)
    do_something();

The comparison operators == will be read first, then the tolerance operators ^ will be applied and finally the boolean || will operate on the two results.

All of the comparison operators can be qualified by the tolerance operator and have the following respective meanings

a ==  b  ^ tolerance        equal within tolerance
a != b ^ tolerance           not equal by tolerance
a < b ^ tolerance           less than by tolerance
a > b ^ tolerance           more than by tolerance
a <= b ^ tolerance       less than, OR equal within tolerance - could be greater than within the tolerance
a >= b ^ tolerance        more than, OR equal within tolerance - could be less than within the tolerance

The variadic product_of< >(...) function

There can be occasions when several quantities expressed in diversely scaled units meet together to form a product. Traditionally this would be dealt with by someone sitting down and consolidating all the the conversions into one factor by hand to optimise execution. When using a units library such as this you are denied such hand tinkering with factors, so it should be capable of doing it for you. You are not going to get this while you write the product in the normal manner as a chain of binary multiplications

C++
mins time_taken2 =  2_Kms_pHour *  3_grams /  4_PoundsForce

because each binary operation gets compiled separately and there is no opportunity to optimise across all of them.

Instead call the product_of<>(...) variadic function which embraces the entire product and will automatically and systematically consolidate all conversions into a single factor during compilation

C++
mins time_taken2 = product_of<mins>(2_Kms_pHour, 3_grams , ulib::divide_by, 4_PoundsForce);

The template argument (in this case mins) is optional and allows you to state the units in which you want the result. This is useful for lines of code such as above because it wraps the final conversion to mins together with the consolidated factors so the entire line of code is executed with only a single factor conversion.

The ulib::divide_by parameter allows the product to include quotients and applies to everything that follows it (as if it were enclosed in brackets). Although you can use it multiple times it is more sensible to arrange things so that it appears just once so that interpretation does not become too confusing.

If you don't specify the return type you want

C++
product_of<>(2_Kms_pHour, 3_grams , ulib::divide_by, 4_PoundsForce);

then it will return the result as an unscaled rational unit of the rational unit system that is most dominant among its argument types.

Measurements against a datum - temperature

Temperatures in Celcius and Fahrenheit are not values that represent a quantity because their zero values don't represent non-existence of temperature. However temperature differences in degrees centigrade and fahrenheit are values that represent a quantity because their zero values do represent non-existence of temperature difference. Furthermore Kelvin is a measurement of temperature but its value does represent a quantity because its zero value represents non-existence of temperature. We should not forget that we didn't always know what non-existence of temperature was.

You may need to read the above paragraph more than once. It is about the distinct identities of temperature (Celcius, Fahrenheit), temperature difference (degrees centigrade and fahrenheit) and absolute temperature (Kelvin). The following operational considerations should clarify the need to respect these distinct identities and that they cannot be treated as the same thing.

  • Conversion between temperature Celcius and Fahrenheit requires a factor and a offset. Conversion between temperature difference in  degrees centigrade and fahrenheit requires only a factor.
  • Temperatures in Celcius and Fahrenheit cannot be used directly in many arithmetic operations because their arbritrary datum will produce arbritrary results as the following demonstrates:
    0 ºC + 0 ºC = 0 ºC
    32 ºF + 32 ºF = 64 ºF which is not equal to 0 ºC
  • Many thermodynamic formulae use Kelivin as a quantity and require it to participate in arithmetic operations as a quantity but it would not be correct to allow this for a temperature expressed in Celcius or Fahrenheit.

All of this had to be considered in deciding what generic capacity the library should provide that will embrace temperature and its datum based measures of Celcius and Fahrenheit, including whether this generic capacity might have application in other dimensions such as LENGTH  or TIME. This is the result:

First we recognise that temperature difference is a proper quantity and can be defined as unit typed quantities. So we define a new dimension of measurement TEMPERATURE

C++
ulib_Base_dimension_4(TEMPERATURE)

and define degrees_C and degrees_F as base and Scaled units for that dimension

C++
//units of temperature difference
ulib_Base_unit(degrees_C, TEMPERATURE)
ulib_Scaled_unit(degrees_F, =, 0.5555555, degrees_C)

Then we define Celcius and Fahrenheit as something different, a datum measurement type, having first called ulib_Name_common_datum_point_for_dimension to establish by description, a common datum that they can both refer to.

C++
//measurements of temperature
ulib_Name_common_datum_point_for_dimension(TEMPERATURE, water_freezes) //All datum measurements of TEMPERATURE will be defined with reference to water freezes  
ulib_datum_Measurement(Celcius, degrees_C, 0, @, water_freezes)  // 0 Celcius at water freezes
ulib_datum_Measurement(Fahrenheit, degrees_F, 32, @, water_freezes) // 32 Fahrenheit at water freezes

A datum measurement is quite distinct from a unit of measurement and supports a different and more limited set of operations representing what you can sensibly do with them.

=                     any datum measurement with the same dimensions (another temperature)

+, +=                any unit type with the same dimensions (increase by a temperature difference)

-,  -=                 any unit type with the same dimensions (decrease by a temperature difference)

-                        any datum measurement with the same dimensions (return difference between two temperatures)

Here they are in action with mixed units and measures:

C++
Celcius T1 = 32_Fahrenheit; //convert Fahrenheit to Celcius with factor and offset

T1 += 10_degrees_C;          //increase a temperature

Fahrenheit T2 = T1 - 32_degrees_F; //return decreased temperature 

degrees_C temp_diff = T1 - T2;     //return difference between two temperatures

Note that there is no direct conversion between Celcius and degrees_C (that would be disastrous), you have to do it by differences as in the example above. 

Datam measures cannot be raised to powers but they support a ::unit type modifier which gives the units in which they are measured so Celcius::units will give you degrees_C.

Finally we come to Kelvin. It is a temperature measurement like Celcius and Fahrenheit but is also a proper quantity whose zero value represents non-existence. We will want it to be convertible between Celcius and Fahrenheit but we will also want to use it as a quantity, participating in products and quotients in thermodynamic expressions. This we define as an absolute measurement with reference to the same datum that we established for Celcius and Fahrenheit.:

C++
ulib_absolute_Measurement(Kelvin, degrees_C, 273, water_freezes)

An absolute measurement supports the same operations as datum measurements above but additionally it supports

*, /                       with any unit typed quantity  - returns a new temporary typed quantity

*, /, *=, /=      with a raw number - returns same absolute measurement type

                            with a raw number as left hand argument - returns same absolute measurement type

/                               with a raw number as left hand argument - returns inverse of underlying units

This allows it to participate in mathematical formulae respresenting a proper quantity of temperature. Note that even with Kelvin, you are not invited to add two temperatures. What would it mean? Are absolute temperatures in any sense additive?

An absolute measurement will implicitly convert to its underlying units

C++
degrees_C arg = 293_Kelvin;

The underlying units can also be converted to an absolute measurement but it must be done explicitly

C++
Kelvin absTemp = Kelvin(293_degrees_C); //affirms that you are reading it as an absolute temperature

Datum measurements do have potential application in other dimensions.

In the dimension of TIME we define secs as the primary base unit, which is really a time difference. To represent position in time we would use date and time which has different datums depending on your culture. These would be datum measurements in the dimension of time. In this case we still have no well understood concept of non-existence of time so there is no scope for defining an absolute measurement.

In construction, specifying points of intersection can somtimes produce less misalignment than specifying the lengths of components and with modern digital surveying equipment it can also be more convenient. These points of intersection will be points in space measured from fixed datums. They are datum measurements in the dimension of LENGTH and would need to be treated as such with length as a quantity only emerging as differences between these datum units. The concept of absolute measurement is of little interest to a builder in this context but an astrophysicsist might see distance from the centre of the Earth as an absolute measurement of elevation.

Writing functions for unit typed quantities

Any code that does very much will inevitably delegate some of the work to be done by calling functions. So as well as being able to write:

C++
metres radius = 5_metres;
sq_metres area_of = 3.14159 * radius* radius;

you will also want to be able to write:

C++
metres radius = 5_metres;
sq_metres area  = area_of_circle(radius);

where  area_of_circle encapsulates the calculation  3.14159 * radius* radius

You won't get this if you define the function to take and return a raw number such as a double.

C++
double area_of_circle(double radius)
{
        return PI*radius*radius;
}

There are no implicit conversions from unit typed quantities to a double and calling it using the protocols for explicit conversion, as_num_in<metres>(radius) and construction of a sq_metres type,  is less than convenient

C++
sq_metres area  = sq_metres(area_of_circle(as_num_in<metres>(radius));

Instead we simply type the function using unit types

C++
sq_metres area_of_circle(metres radius)
{
	return PI*radius*radius;
}

This will produce correct results and ensure that the function is not called with the wrong type of units and that its return value is not misinterpreted. You can call it as follows:

C++
sq_metres area = area_of_circle(5_metres);

You can also use it with other units that measure length

C++
sq_Kms area = area_of_circle(5_Kms);

It will implicitly convert 5_Kms to 5000_metres as the function is called and the return value will be implicitly converted to the declared type of sq_Kms for area.

Here are some more examples:

C++
metres_psec average_speed(metres dist, secs time)
{
	return dist / time;
}

metres distance_moved(metres_psec2 accel, metres_psec init_vel, secs t)
{
	return 0.5 * accel * t* t + init_vel*t;
}

degrees_C max_temperature_diff(Celcius T1, Celcius T2, Celcius T3)
{
	return (abs(T1 - T2) > abs(T2 - T3)) ?
		((T3 - T1) > abs(T1 - T2)) ? (T3 - T1) : abs(T1 - T2)
		:
		((T3 - T1) > abs(T2 - T3)) ? (T3 - T1) : abs(T2 - T3);
}

This one is a recursively implemented variadic function.

C++
template<typename... Args>
inline metres max_length(const metres& arg, Args const &... args)
{
	auto max_of_rest = max_length(args...);
	return (arg >= max_of_rest) ? arg : max_of_rest;
}
inline metres max_length(const metres& arg)
{
	return arg;
}

If you are committed to an MKS context for any intensive calculations then this is a perfectly adequate approach. All of the functions have been sensibly typed using MKS units so that calling them with MKS units will not invoke conversions and having scaled units convert to MKS as soon as they get involved in any serious calculations is very much in line with that approach.

However for the wider scope embraced by this library in which scaled units can be persisted and multiple rational unit systems can be used, they are too narrowly defined. It is not desirable that:

C++
sq_Kms area = area_of_circle(5_Kms);

invokes conversion to metres on calling area_of_circle and then a conversion back to sq_metes in the assignment of area because the expression that it encapsulates, PI*radius*radius , will evaluate in whatever units it is passed, without conversions. It is only your function's insistence on taking metres that is forcing these conversions.

It is even worse that having defined cms as cgs rational unit:

C++
cms v = area_of_circle(5_cms);

will still face a conversion to metres and back.

In both cases it is the insistance that the function be passed a length in metres that forces these unwanted and otherwise unecessary conversions.

We can remove this requirement for metres by defining the function more generically to take any unit of length:

C++
template<class T>
auto area_of_circle(LENGTH::unit<T> radius) //any unit of length
{
	return PI*radius*radius;
}

this function will instantiate for a radius in any unit of length and pass it into the function without conversions. So

C++
sq_Kms area = area_of_circle(5_Kms);

will instantiate it for Kms will call and evaluate without conversions returning a value in sq_Kms.

If a function has more than one input parameter then you will need a template argument to assiciate with the generic signature for each one:

C++
template<class T1, class T2>
auto average_speed(LENGTH::unit<T1> dist, TIME::unit<T2> time ) 
{
	return dist / time;
}

We have already defined the elemental dimensions of LENGTH, MASS, TIME and TEMPERATURE because these are needed to establish the base units from which all others are defined. To facilitate generic function definitions it is useful to define the compound dimensions that we are going to need. This is done following the same logic as unit definitions as follows:

C++
using AREA = LENGTH::squared;
using VOLUME = LENGTH::cubed;
using FREQUENCY = TIME::inverted;
using DENSITY= MASS::Divide<VOLUME>;

using VELOCITY = LENGTH::Divide<TIME>;
using ACCELERATION = VELOCITY::Divide<TIME>;
using FORCE = ACCELERATION::Multiply<MASS>;
using ENERGY = FORCE::Multiply<LENGTH>;
using POWER = ENERGY::Divide<TIME>;

and now we can get on with writing a wider range generic functions

C++
template<class T1, class T2, class T3>
auto distance_moved(ACCELERATION::unit<T1> accel,
			VELOCITY::unit<T2> init_velocity,
			TIME::unit<T3> t)
{
	return 0.5 * accel * t* t + init_velocity*t;
}

template<class T1, class T2, class T3>
auto max_temperature_diff(TEMPERATURE::measure<T1> t1, 
			TEMPERATURE::measure<T2> t2, 
			TEMPERATURE::measure<T3> t3)
{
	return (abs(t1 - t2) > abs(t2 - t3)) ?
		((t3 - t1) > abs(t1 - t2)) ? (t3 - t1) : abs(t1 - t2)
		:
		((t3 - t1) > abs(t2 - t3)) ? (t3 - t1) : abs(t2 - t3);
}

note that if we want to pass temperatures like Celcius or Fahrenheit then we define it as a TEMPERATURE::measure<T1> rather than as a unit. A TEMPERATURE::unit<T1> would degrees_C or degrees_F

you can do the same with variadic functions too

C++
template<class T, typename... Args>
inline auto max_length(const LENGTH::unit<T>& arg, Args const &... args)
{
	auto max_of_rest = max_length(args...);
	return (arg >= max_of_rest) ? arg : max_of_rest;
}
template<class T>
inline auto max_length(const LENGTH::unit<T>& arg)
{
	return arg;
}

Generic function definitions are efficient across a wider range of contexts and most importantly are guaranteed not to obstruct the factor free evaluation of any rational unit system. They are the choice for short functions that may be repeatedly executed in unknown and demanding contexts. Where a function is large and executes a lot of code, the gains of avoiding entry conversions become proportionally smaller and code bloat can become a problem so it is better to define fixed (non-generic) type functions.

Calling non unit typed functions

It likely that you have many functions already written that you want to use but of course they take and return raw numbers usually typed as doubles. 

In many cases you can easily convert them to unit typed functions. You just have to type the input parameters as appropriate unit types and the return value and any local variables as auto. This is particularly appropriate where the functions are short and encapsulate universal laws, as with the examples in the last section

However there are many reasons why you may not be able do this:

  • You don't have access to the source code of the function. 
  • The implementation of the function involves mathematical abstractions which unit types cannot represent.

Or may not want to:

  • The function is maintained and updated by somebody else so you don't want your code to be working with an unmaintained copy.
  • Some of your code will still need to call the original version and you don't want to maintain two versions.
  • The function is complex but works reliably and you don't want to mess with it.

As stated earlier, you cannot pass unit typed quantities directly into functions that take raw numbers. You have to explicitly extract the numerical values using  as_num_in<unit_type>(quantity) specifying the units in which you want them and will have to make sure that they are correct for the function being called.

To illustrate this let us contrive an example:

C++
double mass_of_material_required(double area, double height, double density) 
{
    return 1.09 *  area * height * density;
}

where 1.09 represents wastage in an industrial process and will be adjusted when improvements are made. The key thing here is that somebody else is responsible for updating this function, and that is all they will do. So it is important that you call the version that they are maintaining and not some copy that you have made.

You can call it directly:

C++
Kgs Mass = Kgs(
                mass_of_material_required(
                   as_num_in<sq_metres>(area), 
                   as_num_in<metres>(height), 
                   as_num_in<Kgs_pcubic_metre>(density))
               );

That is ok if you are only going to call it once. Not only is it ugly and verbose to use repetitively but also a great deal of diligence must be applied to ensuring that the calls to as_num_in<units>(quantity) are requesting the correct units and that you construct the correct unit type from the return value. Getting this wrong will produce wrong results with no warnings or errors.

For this reason it makes sense to write a wrapper to call this function. This way you do the diligence just once ,

C++
Kgs mass_of_material_required( sq_metres area, metres height, Kgs_pcubic_metre density) 
{
    return Kgs(
            mass_of_material_required(
                as_num_in<sq_metres>(area), 
                as_num_in<metres>(height), 
                as_num_in<Kgs_pcubic_metre>(density))
            ) 
}

and have clean function calls in your code. 

C++
Kgs Mass = mass_of_material_required(area, height, density);

Functions can be specific and peculiar about the units with which they work and you should always research that before wrapping them. However many of them will fit into one of two categories:

  • They work with more than one dimension of measurement and require that the input parameters are all unscaled rational units of the same rational unit system and will return a result in  unscaled rational units of the same rational unit system. The  mass_of_material_required function above is an example.
  • They  work in only one dimension of measurement, require that all input parameters are expressed in the same units and will return a result in those same units. An example would be the following:

    C++
    auto GetMaxTempDiff(Celcius t1, Celcius t2, Celcius t3)
    {
            return degrees_C(
                    get_max_difference_between_three_numbers(
                            as_num_in<Celcius>(t1), 
                            as_num_in<Celcius>(t2),
                            as_num_in<Celcius>(t3))
            );
    }
    which calls a numerical routine  get_max_difference_between_three_numbers which we can imagine may be more optimal than the  GetMaxTempDiff function I wrote earlier.

Both of these categories are candidates for more generic definitions but they require different approaches:

The generic wrapper prototype for  GetMaxTempDiff will be 

C++
template<class T1, class T2, class T3>
auto GetMaxTempDiff(TEMPERATURE::measure<T1> t1, TEMPERATURE::measure<T2> t2, TEMPERATURE::measure<T3> t3);

But how do we implement the call to get_max_difference_between_three_numbers?
The following may look neat and tidy 

C++
return degrees_C(
                get_max_difference_between_three_numbers(
            //Don't do this!!
                        as_num_in<TEMPERATURE::measure<T1>>(t1), 
                        as_num_in<TEMPERATURE::measure<T2>>(t2), //No don't
                        as_num_in<TEMPERATURE::measure<T3>>(t3)) //No don't
        );

but it is very wrong. get_max_difference_between_three_numbers must recieve all its input parameters in the same units and code above allows three different measurements of temperature to be sent straight into the function. The following is correct:

C++
return degrees_C(
                get_max_difference_between_three_numbers(
            
                        as_num_in<TEMPERATURE::measure<T1>>(t1), 
                        as_num_in<TEMPERATURE::measure<T1>>(t2), 
                        as_num_in<TEMPERATURE::measure<T1>>(t3)) 
        );

but sits in danger of being 'corrected' because it looks like a mistake has been made. For this reason I recommend a more explicit statement of intention;

C++
using working_type = TEMPERATURE::measure<T1>;
return degrees_C(
                get_max_difference_between_three_numbers(
            
                        as_num_in<working_type>(t1), 
                        as_num_in<working_type>(t2), 
                        as_num_in<working_type>(t3)) 
        );

Using the first parameter as the working type is a simple way of getting quite good results but isn't optimal under all circumstances. For best results we use the variadic type deducer ulib::best_fit_type<...>. So a fully optimal wrapper will look like this:

C++
template<class T1, class T2, class T3>
auto GetMaxTempDiff(
        TEMPERATURE::measure<T1> const& t1,
        TEMPERATURE::measure<T2> const& t2,
        TEMPERATURE::measure<T3> const& t3
)
{
        using working_type = ulib::best_fit_type<
                TEMPERATURE::measure<T1>,
                TEMPERATURE::measure<T2>,
                TEMPERATURE::measure>T3>
        >;
        return working_type::units
        (
                get_max_difference_between_three_numbers(
                        as_num_in<working_type>(t1),
                        as_num_in<working_type>(t2),
                        as_num_in<working_type>(t3)
                        )
        );
}

Functions that work with more than one dimension of measurement cannot insist that all parameters are in the same units but will often insist that they are all unscaled rational units of the same rational unit system. A generic protocol is provided for this as follows:

C++
template<class T1, class T2, class T3>
auto mass_of_material_required(AREA::unit<T1> area,
                        LENGTH::unit<T2> height,
                        DENSITY::unit<T3> density)
{
      enum {
                working_sys = ulib::best_fit_rational_unit_system<
                                        AREA::unit<T1>, 
                                        LENGTH::unit<T2>,
                                        DENSITY::unit<T3>
                                >()
        };

        return MASS::rational_unit<working_sys>
        (
                mass_of_material_required(
                        as_num_in<AREA::rational_unit<working_sys>>(area),
                        as_num_in<LENGTH::rational_unit<working_sys>>(height),
                        as_num_in<DENSITY::rational_unit<working_sys>>(density)
                        )
        );
}

First we have to decide in which rational unit system it will be evaluated. ulib::best_fit_rational_unit_system<...>() will provide the best fit to the types passed in. Then all inputs and outputs to the numerical function called are typed as rational units of that system in the appropriate dimension using the ::rational_unit<working_sys> type qualifier.

Maths functions

Some simple maths functions are provided for convenience. 

square_of(quantity), cube_of(quantity), inverse_of(quantity), to_power_of<n>(</n>quantity) will operate on any unit typed quantity and return a result in the correct units.

C++
sq_metres area = square_of(5_metres);
cubic_metres volume = cube_of(5_metres);
Hertz frequency = inverse_of(0.05_secs);

sqrt(quantity) and int_root<n>(</n>quantity) will operate on any unit typed quantity whose root can be expressed in valid units

C++
metres len = sqrt(20_sq_metres);
metres len = int_root<3>(20_cubic_metres);
auto res = sqrt(5_metres); //ERROR – no valid unit type for root

These functions use the generic signature for units of any type. They are not defined for datum measures because they would not be valid operations.

C++
template <class T>
inline constexpr typename ulib::unit<T>::squared square_of(ulib::unit<T> const& u)
{
	return u*u;
}

There are some mathematical procedures that cannot be carried out with unit typed quantities or measures because their intermediate values can have no valid expression as units or measures. So functions that perform exotic mathematical transformations may have to written as purely numerical functions and then wrapped by a unit typed function that calls it. There is one very ordinary operation that runs into this problem and that is finding the average of a set of temperatures.

The normal and most efficient way to calculate an average temperature is to add them all up and divide by how many there are. However this library will not allow you to add temperatures (Celsius or Fahrenheit) together because that would produce results that in the general case are not safe to use. Remember:

0 ºC + 0 ºC = 0 ºC
32 ºF + 32 ºF = 64 ºF which is not equal to 0 ºC

There is no problem with the concept of an average temperature. It is just the intermediate process of adding them together that it won't, and can't go along with. There are two ways around this:

Convert the temperatures (Celsius or Fahrenheit) to degrees_C or degrees_F and then they can be added

C++
Celcius average_of_three(Celsius t1, Celsius t2, Celsius t3)
{
	return 0_degrees_C + ((t1  - 0_degrees_C) + (t2  - 0_degrees_C) +(t3  - 0_degrees_C))/3 ;
}

but this adds extra subtractions and additions in the executable code.

The other is to leave the sanctuary of unit typed quantities to perform the calculation efficiently

C++
Celcius average_of_three(Celsius t1, Celsius t2, Celsius t3)
{
	return degrees_C ( 
			(as_num_in<Celsius>(t1)
			+ as_num_in<Celsius>(t2)
			+ as_num_in<Celsius>(t3)) / 3
		)
	);
}

Either way it is a lot of inconvenience for such a simple requirement so a variadic mean_of (...) function is provided that will work with any unit or measure typed quantity.

C++
Celcius ave_temp = mean_of(t1, t2, t3);

and for good measure, variadic functions for variance and standard deviation

C++
degrees_C::squared  variance =  variance_of(t1, t2, t3);
degrees_C  deviation =  std_deviation_of(t1, t2, t3);

 

User interface support

There are two new features that enable units of measurement correctness to be taken to the user interface:

The first provides safety by giving you the means to systematically display the units in which quantities are measured. It is a template function that will return the name of any unit or measure type as a null terminated string of chars.

C++
template <class Unit> constexpr char* get_unit_name();

This can be used to systematically display or print the names of units and measures alongside the values measured in them. For instance to display a quantity measured in metres, the display value will be as_num_in<metres>(quantity) and the text for its units label will be get_unit_name<metres>()

The second provides flexibility to the user without breaking that safety. It safely encapsulates the initialisation of combo boxes that give the user a choice of alternative display units.

It is a function that is best specified together with the lambda prototype that it works with.

C++
ulib::for_each_compatible_unit<ref_unit>
(
	[this](
			char* szName, 
			run_time_conversion_info<ref_unit> converter,
			bool is_ref_unit
		)
	{    //Your code goes here
		//to do - copy szName into combo list

		//to do - store converter so index aligns with string in combo

		//to do - set selection if ref_unit
				
	}
);
</ref_unit>

ref_unit is the unit type in your code which you are displaying. For each compatible unit or measure that it finds among those you have defined, it will call the body of the lambda passing:

  • its name as a zero terminated character string.
  • a run_time_conversion_info<ref_unit> object named converter
  • a boolean indicating if the unit passed is the ref_unit

You use the call to populate the combo dropdown with unit names and associate a run_time_conversion_info<ref_unit> object with each one so that when a unit name is selected, its associated run_time_conversion_info<ref_unit> object can be referenced. The run_time_conversion_info<ref_unit> object exports just two functions

C++
double to_display_units(ref_unit const& u)

which you should use to write the quantity referenced in your code to the screen

C++
ref_unit from_display_units(double value_read)

which you should use to read from the screen to the quantity referenced in your code Each one will provide the correct conversions required for the units selected in the combo dropdown. This approach is exemplified by the MeasurementControl class in the example application.

Using the code

You will need to include <type_traits> from the standard library and "ulib_4_11.h" downloaded from this article before defining your units. You will also need to include <cmath> from the standard library if you want to use the sqrt or integer_root functions. It is a good idea to have separate header in which you define your units, say my_units.h. So the include list will be:

C++
#include <type_traits>
#include <cmath>
#include "ulib_4_11.h"
#include "my_units.h"

If you want to enclose your unit definitions in a namespace then you must enclose "ulib_4_11.h" in the same namespace, but not the standard library headers, like this:

C++
#include <type_traits>
#include <cmath>
namespace my_units
{
	#include "ulib_4_11.h"
	#include "my_units.h"
}

By default 7 dimensions are available for use. If you need more then you must define ULIB_DIMENSIONS before #include "ulib_4_11.h" indicating the number of dimensions as follows

C++
#define ULIB_DIMENSIONS ULIB_9_DIMS
#include "ulib_4_11.h"

You can specify up to 15 dimensions. If you want to go beyond that then you have to add some extra lines to the concatation_macros in "ulib_4_11.h".

You may also indicate that you need less than 7 dimensions and this will save the compiler a bit a work. The examples in this article require only 5 dimensions so the header list runs:

C++
#include <type_traits>
#include <cmath>

#define ULIB_DIMENSIONS ULIB_5_DIMS
#include "ulib_4_11.h"
#include "my_units.h"

The “my_units.h” header should follow the following protocol:

define the fundamental dimensions of measurement

C++
ulib_Base_dimension1(LENGTH)
ulib_Base_dimension2(MASS)
ulib_Base_dimension3(TIME)
ulib_Base_dimension4(TEMPERATURE);
ulib_Base_dimension5(ANGLE);

define any secondary rational unit systems

C++
ulib_Secondary_rational_unit_systems(cgs, Kmsmins)

this can be omitted if there aren't any

Begin the unit definitions

C++
ulib_Begin_unit_definitions
  • define your units beginning with the base units
    C++
    ulib_Base_unit(metres, LENGTH)
    ulib_Base_unit(Kgs, MASS)
    ulib_Base_unit(secs, TIME)
    and build your other unit definitions from them using the following macros
    C++
    ulib_Compound_unit(metres_psec, =, metres, Divide, secs)
    ulib_Scaled_unit(mins, =, 60, secs)
    ulib_Unit_as_square_of(sq_metres, metres)
    ulib_Unit_as_cube_of(cubic_metres, metres)
    ulib_Unit_as_inverse_of(Herz, secs)
    
    ulib_Enable_datum_measures_for_dimension(TEMPERATURE, water_freezes)
    ulib_Datum_measurement(Celcius, degrees_C, 0, @, water_freezes)
    ulib_Absolute_measurement(Kelvin, degrees_C, 273, @, water_freezes)
    
    ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)
    ulib_Secondary_base_adopt_base_unit(cgs, secs)

End the unit definitions

C++
ulib_End_unit_definitions

A type list is built of all the types you have defined between ulib_Begin_unit_definitions and ulib_End_unit_definitions. Currently this is only used by the ulib::for_each_compatible_unit function used to populate combo boxes. You may define unit and measure types after ulib_End_unit_definitions but they will not be included in this list. Also only up to 300 lines will be scanned when generating this list and any units beyond that limit will not be included.

To avoid name clashes in the global namespace:

  • All macros are prefixed by the ULIB_ or ulib_ moniker
  • All library functions are either global and typed to take only the unit typed quantities defined in this library or sit within the ulib namespace
  • All types and type modifiers either sit in the ulib namespace or are modifiers of the units, measures and dimensions of measurement that you define. 

Now you can start typing your quantities with the units they are measured in.

The example application

The example application demonstrates how unit typed quantities can be brought to the user interface in a safe but flexible manner. It is implemented using my own library for dialogs and controls Windows dialog design in C++ without dialog templates also published on The Code Project. The library files are supplied with the application and no resource files or IDE generated files are required.

Building the application

It is delivered as a Microsoft Visual Studio Express 2015 project - units and measures.sln. If your C++ development environment doesn't understand the units and measures.sln file then simply create a new empty project and copy in the files:

autodlg.h
autodlg_controls.h
autodlg_metrics_config.h
measurement_ctrl.h
MyUnits.h
ulib_4_11.h
units_gui_examples.h

units and measures demo.cpp

and set units and measures demo.cpp as the file to compile. You may need to adjust the entry point function to match that of the empty project. It may be something other than _tWinMainYou can remove thw .rc and resource.h files from your project, they will not be used.

The custom control for unit typed quantities

measurement_ctrl.h contains the code for a generic unit typed compound control that displays the value of a quantity next to the units it is expressed in and allows the user to select alternative display units from a combo box list. This shows how the function ulib::for_each_compatible_unit can be used to populate a combobox.

C++
	ulib::for_each_compatible_unit<ref_unit>
	(
		[this](
			char* szName, 
			run_time_conversion_info<ref_unit> converter,
			bool is_ref_unit
		)
		{
			// add unit name to combo
			cmbUnit.do_msg(CB_INSERTSTRING, 0, static_cast<wchar_t *>(wchar_buf(szName)));
			//store converter so index aligns with string in combo
			converters.insert(converters.begin(), converter);
			//set selection if ref_unit
			if (is_ref_unit)
				cmbUnit.do_msg(CB_SETCURSEL, 0);
		}
	);
</ref_unit>

and build and use an array of converters for each selection that the user may make.

C++
std::vector<run_time_conversion_info<ref_unit>> converters;

to be used when writing to and reading from the display

C++
ref_unit& read()
{
    int iSel = (int)cmbUnit.do_msg(CB_GETCURSEL);
    if (iSel > -1)
    {
        the_quantity = converters[iSel].from_display_units(_tstof(edtNum.as_text));
    }
    return the_quantity;
}
template<class unit>
void write(unit const& u)
{
    int iSel = (int)cmbUnit.do_msg(CB_GETCURSEL);
    if (iSel>-1)
    {
        the_quantity = u;
        edtNum.as_text =
            wchar_buf(
                converters[iSel].to_display_units(the_quantity)
        );
    }
}
The dialogs

The first dialog New_road_dlg is based in the road building scenario descibe in the Overview section. It is implemented in the most concise way, reading and writing directly from the controls.

C++
template <class metrics = autodlg::def_metrics>
class New_road_dlg : public autodlg::dialog < metrics, autodlg::auto_size, WS_THICKFRAME>
{
	AUTODLG_DECLARE_CONTROLS_FOR(New_road_dlg)
		//input
		ULIB_MEASUREMENT_CTRL(edtWidth_of_road, at, hGap, vGap, metres)
		ULIB_MEASUREMENT_CTRL(edtCoverage_of_tarmac, 
                     to_right_of<_edtWidth_of_road>, hGap, 0, sq_metres)
		ULIB_MEASUREMENT_CTRL(edtTransit_time, 
                     under<_edtCoverage_of_tarmac>, 0, 2*vGap, secs)
		//output
		ULIB_MEASUREMENT_CTRL(edtLength_of_road, 
                     under<_edtWidth_of_road>, 0, BWidth * 3 / 2, Kms)
		ULIB_MEASUREMENT_CTRL(edtSafe_velocity, 
                     to_right_of<_edtLength_of_road>, hGap, 0, Kms_pHour)

		AUTODLG_CONTROL(btnCalculate, 
         at, BWidth, BWidth * 5 / 4, BWidth, BHeight, BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

	AUTODLG_END_DECLARE_CONTROLS

	void OnInitDialog(HWND hWnd)
	{
		edtWidth_of_road() = 10_metres;
		edtCoverage_of_tarmac() = 20000_sq_metres;
		edtTransit_time() = 90_secs;
		
		edtLength_of_road().read_only();
		edtSafe_velocity().read_only();
		
		btnCalculate.notify(BN_CLICKED);
	}
	void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
	{
		if (BN_CLICKED == NotifyCode)
		{
			edtLength_of_road() = edtCoverage_of_tarmac().quantity() / edtWidth_of_road().quantity();
			edtSafe_velocity() = edtLength_of_road().quantity() / edtTransit_time().quantity();
		}
	}
};

This approach would be appropriate for one off user interactive anecdotal calculations.


The second dialog Free_fall_dlg calculates distance and speed while falling under gravity. It initialises a quantity from the control holding time as you would do in preparation for intensive calculations (reading and writing from controls is always slow).

C++
void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
{
	if (BN_CLICKED == NotifyCode)
	{
		secs t ( edtTime_elapsed());

		edtDistance_fallen() = distance_fallen(t);
		edtVelocity_reached() = falling_velocity(t);
	}
}

It also calls some unit typed functions.

C++
constexpr metres_psec2 gravity(9.8);

metres distance_fallen(secs const& t)
{
	return 0.5 * gravity * t* t;
}
metres_psec falling_velocity(secs const& t)
{
	return gravity * t;
}

The types are fixed as MKS because the internally referenced constant gravity is hard wired to MKS.


The third dialog Mass_and_energy_dlg deals with any accelerating body and performs a force and energy analysis and reconciliation. In this case the input values are used multiple times in the analysis so to avoid re-reading the controls, their quantities are assigned to simple unit typed variables. Simple unit typed variables are also used to hold intermediate values that will see further multiple use. Finally the output controls are assigned from calculations using those intermediate values.

C++
void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
{
	if (BN_CLICKED == NotifyCode)
	{
		secs t (edtTime_elapsed());
		metres_psec2 a(edt_Acceleration());
		Kgs m( edt_Mass_moved());

		metres d = distance_moved(a, 0_metres_psec,  t);
		metres_psec v = velocity_reached(a, 0_metres_psec, t);
		Newtons f = a * m;

		edtDistance_moved() = d;
		edtVelocity_reached() = v;
		edtKinetic_energy() = 0.5 * m * v * v;
		edtForce_required() = f;
		edtEnergy_consumed() = f * d;
		edtPower_required() = f * v;
	}
}

In this case the intermediate values are evaluating using calls to generically defined functions.

C++
template<class T1, class T2, class T3>
auto distance_moved(
	ACCELERATION::unit<T1> accel,
	VELOCITY::unit<T2> init_velocity,
	TIME::unit<T3> t)
{
	return 0.5 * accel * t* t + init_velocity*t;
}
template<class T1, class T2, class T3>
auto velocity_reached(
	ACCELERATION::unit<T1> accel,
	VELOCITY::unit<T2> init_velocity,
	TIME::unit<T3> t)
{
	return accel * t + init_velocity;
}

You may notice that kinetic energy is evaluated in place. You can do that too. You don't have to call a function.


The fourth dialog Rotational_power_dlg illustrates an unconventional approach to defining units for use with rotating systems. Conventional wisdom has it that an angle (in radians) is a ratio between two lengths and therefore is a scalar with no dimensions. That metaphysical assertion doesn't sit well with the fact that we measure angle and do so in different ways and it also leads to a very error prone dimensional identity between Energy (1Newton moved through 1 metre) and Torque (1 Newton applied at a radius of 1 metre). This confusion arises because the 1 metre plays a very different role in the definition of Energy to that which it plays in the definition of Torque. One is the distance through which the force moves and the other is the radius at which it is applied.

My feeling was that this doesn't properly represent the role of angle and there is a lack of distinction between a metre of radius (a geometrical property) and a metre in the line of action (a journey made). I decided to have a dimension called ANGLE and call the metres of radius radial_metres and then figure out how they should be related to make everything work.

In Units of Measurement types in C++ I approached this by looking for dimensional equality on Energy, the result I wanted. However the relationship between distance along the line of action, angle turned and the radial distance is a simple geometrical one.

distance along the line of action = angle turned x radial distance

so rearranging the expression to express radial distance

radial distance = distance along the line of action / angle turned

If we affirm that distance along the line of action has dimensions of LENGTH, then radial distance must have dimensions of LENGTH / ANGLE

So we add a dimension of ANGLE

C++
ulib_Base_dimension5(ANGLE);

and a primary base unit of radians

C++
ulib_Base_unit(radians, ANGLE)

and our definition of radial_metres

C++
ulib_Compound_unit(radial_metres, =, metres, Divide, radians)//RADIAL_LENGTH

Now we can properly define Torque

C++
ulib_Compound_unit(Nm_torque, =, Newtons, Multiply, radial_metres)//TORQUE

If you do the dimensional analysis you will find that this leaves a torque turning an angle with the same dimensions as a force acting over a distance. They have dimensional equality on Energy.

The example is quite simple. It calculates power output of an engine given its rotational speed and the braking torque that needs to be applied to hold it at that speed. It uses the formula

power = torque * angular velocity. So it defines input controls

C++
ULIB_MEASUREMENT_CTRL(edtRotational_velocity, at, hGap, vGap, radians_psec)
ULIB_MEASUREMENT_CTRL(edtBraking_torque, under<_edtRotational_velocity>,
			0, BHeight, Nm_torque)

and an output control

C++
ULIB_MEASUREMENT_CTRL(edtPower_output, under<_edtRotational_velocity>,
			0, BWidth * 3 / 2, Watts)

Which is calculated as the product of the two input quantities.

C++
void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
{
	if (BN_CLICKED == NotifyCode)
	{
		edtPower_output() = edtBraking_torque().quantity() * edtRotational_velocity().quantity();
	}
}

Although Watts and radians_psec are units that lend themselves to clear theoretical analysis (you can think about them without worrying about factors) the people recording the measurements will probably feel more comfortable working with revolutions per minute and horsepower. Accordingly revolution and horsepower are defined as scaled units.

C++
ulib_Scaled_unit(revolutions, =, 2 * 3.14159, radians)
ulib_Compound_unit(revs_pmin, =, revolutions, Divide, mins)
ulib_Scaled_unit(HorsePower, =, 735.499, Watts)

and as a result of that they will automatically appear in the appropriate combo box unit selection lists.


Finally Units_of_measurement_input_output_demo is a tabbed dialog that contains and displays the above dialogs.

Quick Reference

Unit definition macros
see Defining units and Multiple rational unit systems
ulib_Base_unit(new_unit_name, base_dimension)
base_dimension must be a fundamental dimension, not a compound one.
C++
ulib_Base_unit(metres, LENGTH)

ulib_Compound_unit(new_unit_name, =, left, operation, right)
operation may be Multiply or Divide. left and right may be any unit type.
C++
ulib_Compound_unit(metres_psec, =, metres, Divide, secs)
ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)

ulib_Unit_as_power_of(name, =, orig, P) and its deriratives.
orig may be any unit type – P must be integral
C++
ulib_Unit_as_square_of(sq_metres, metres)
ulib_Unit_as_cube_of(cubic_metres, metres)
ulib_Unit_as_inverse_of(Herz, secs)

ulib_Scaled_unit(name, =, unit_as_orig_units, orig)
orig may be any unit type
C++
ulib_Scaled_unit(mins, =, 60, secs)

ulib_Secondary_base_unit(secondary_system, new_unit_name, =, _factor, existing_base_unit)
existing_base_unit must be an unscaled existing base unit
C++
ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)

ulib_Secondary_base_adopt_base_unit(secondary_system, existing_base_unit)
existing_base_unit must be an unscaled existing base unit
C++
uulib_Secondary_base_adopt_base_unit(cgs, secs)
Datum measure definition macros
see Measurements against a datum
ulib_Name_common_datum_point_for(dimension, name_of_datum_point)
name_of_datum_point should be a descriptive identifier
C++
ulib_Name_common_datum_point_for(TEMPERATURE, water_freezes)

ulib_Datum_measurement(name, existing_unit, _offset, at, name_of_datum_point)
name_of_datum_point must be that defined for the dimension of the existing_unit
C++
ulib_Datum_measurement(Celcius, degrees_C, 0, @, water_freezes)
ulib_Datum_measurement(fahrenheit, degrees_F, 32, @, water_freezes)

ulib_Absolute_measurement(name, existing_unit, _offset, at, name_of_datum_point)
name_of_datum_point must be that defined for the dimension of the existing_unit
C++
ulib_Absolute_measurement(Kelvin, degrees_C, 273, @, water_freezes)
Type modifiers
For unit types - see Declarations and initialisation
C++
metres::squared
metres::cubed
metres::to_power_of<4>
sq_metres::int_root<2>

For datum measure types - see Measurements against a datum
C++
Celcius::units //units in which measurement is made (degrees_C)

For dimensions of measurement -see Writing functions for unit typed quantities

C++
using AREA = LENGTH::squared;
using VOLUME = LENGTH::cubed;
using FREQUENCY = TIME::inverted;
using VELOCITY = LENGTH::Divide<TIME>;
using FORCE = ACCELERATION::Multiply<MASS>;
Global functions
as_num_in<type>(quantity_or_measure)</type>. - see Declarations and initialisation
Extracts numerical value from any unit typed quantity or datum measure.
Will convert to requested units if compatible (measures the same thing) otherwise throws compiler error.
C++
double numerical_value_in_Kms = as_num_in<Kms>(5500_metres); 

abs(quantity) returns the absolute positive value
C++
metres absolute_value = abs(-5_metres);

powers and roots - see  Maths functions
C++
sq_metres Area = square_of(5_metres);
cubic_metres Volume = cube_of(5_metres);
cubic_metres Volume = to_power_of<3>(5_metres);

metres side_of_square = sqrt(25_sq_metres); 
//fails if operand is not a squared type
metres edge_of_cube = integer_root<3>(625_cubic_metres) 
//fails if operand is not rootable type

Variadic product_of<>(...) function - see The variadic product_of< >(...) function
Optimises multiple products of quantities in diversely scaled units.
Will take any number of arguments that may be any unit type or number.
C++
mins time_taken2 = product_of<mins>(2, 2_Kms_pHour, 3_grams , divide_by, 4_PoundsForce);
metres dist = product_of<>(2_Kms_pHour, 30_mins, 10.0, divide_by, 2);

Variadic statistical functions. These will take both unit typed quantities and datum based measures
  - see  Maths functions

C++
Celcius average_temperature = mean_of(120_Celcius, 230_Fahrenheit, 300_Kelvin ); 
//because you can't add temperatures

metres::squared variance = variance_of(12.5_metres, 13.3_metres, 11.6_metres);

metres standard_deviation = std_deviation_of(12.5_metres, 13.3_metres, 11.6_metres);
Generic function definitions
see Writing functions for unit typed quantities
within the scope of generic signature
template<class Tn> LENGTH::unit<Tn> //any unit measuring LENGTH
template<class Tn> TEMPERATURE::measure<Tn> //any datum measure of TEMPERATURE
template<class T> ulib::unit<T> //any unit type
template<class T> ulib::measure<T> //any datum measure type
For wrappers of numerical functions
see Calling non unit typed functions
ulib::best_fit_type<...> provides the most numerous (left biased) type out of the parameter list
Use to determine the best fit working type when all measure the same thing but units may be scaled differently.
C++
using working_type = ulib::best_fit_type<
            TEMPERATURE::measure<T1>,
            TEMPERATURE::measure<T2>,
            TEMPERATURE::measure>T3>
ulib::best_fit_rational_unit_system<...>() returns the best fit rational unit system out of the parameter list.
Use where the types handled measure different things.
C++
enum {
              working_sys = ulib::best_fit_rational_unit_system<
                                      AREA::unit<T1>,
                                      LENGTH::unit<T2>,
                                      DENSITY::unit<T3>
                              >()
      };
Dimension::rational_unit<sys> provides the rational unit for a given dimension in the given rational unit system.
C++
LENGTH::rational_unit<working_sys>;
VELOCITY::rational_unit<working_sys>;

How it works

If you really want to know how it all works then you have to examine the code. There are few comments but it is neat and tidy. I will provide some orientation here by outlining the main pillars of its internal architecture.

Dimensions

Barton and Nackman's approach to dimensional analysis is encapsulated by an abstract generic dimensions<> class

C++
template <int d1, int d2, int d3 .....>
struct dimensions
{
	enum {D1 = d1, D2 = d2, D3 = d3 .....};

	….......
}

When you define a base dimension

C++
ulib_Base_dimension1(LENGTH)

you define LENGTH as a specialisation of dimensions<>

C++
using  LENGTH = dimensions<1, 0, 0, ….>;

LENGTH and any other dimensions of measurement you define are not data types. They are abstract types referenced only during compilation.

Unit typed quantities

The data type for all unit typed quantities is the template class ulib::unit<>. Here is its prototype:

C++
template <
		class T, //description class of unit
		class dims = typename T::dims,
		int Sys = T::System
	> class unit; 

It is the class T (the unit description class) that determines which unit it represents.

When you define a unit

C++
ulib_Base_unit(metres, LENGTH)

the following code is generated

C++
struct  _metres
{
	using dims = LENGTH;
	enum { System = 1};

	enum { is_conv_factor = 0};
	static constexpr double to_base_factor=1.0;

	using base_desc = _metres;
	enum { power = P};
};
using metres = ulib::unit<_metres>;	

A description class is defined with a mangled name _metres and your chosen name, metres, is defined as a ulib::unit<> passed the description class as its first template parameter.

When you define a scaled unit

C++
ulib_Scaled_unit(feet, =, 0.3048, metres)

the following code is generated

C++
struct  _feet
{
	using dims = _metres::dims;
	enum { System = _metres::System};

	enum { is_conv_factor = 1};
	static constexpr double to_base_factor=0.3048;

	using base_desc = _feet;
	enum { power = 1};
};
using feet = ulib::unit<_feet>;	

The description classes hold compile time information about the unit being defined.

  • dims holds the dimensions<> that it measures

    System holds an integer representing the rational unit system with which it is associated.

    is_conv_factor is zero if there is no conversion to base units and non-zero if there is. This provides a separate logical indication of the need to convert.

    to_base_factor holds the conversion factor as static constexpr double class member

    base_desc and power are part of a mechanism to maintain the identity of unit types raised to powers.

Having to define a description class 'on the fly' and pass it in, is unavoidable. Although C++11 now allows static constexpr double class members, it still does not allow floating point numbers as template parameters. This means that the conversion factor can never be passed into ulib::unit as a template parameter. You have to write out a new class with the factor initialised with a literal value and pass that in instead. Having established the need for this type indirection, there are design advantages in putting all of the unit specific information in those classes. Above all it means that a ulib::unit<T> is fully defined by its first template parameter with the others defaulting to values that it provides. The class dims = typename T::dims and int Sys = T::System template parameters are there to allow type filtering on the dimensions and rational unit system during overload resolution.

C++
template <class T> function_taking_any_length(ulib::unit<T, LENGTH> arg);

at the user level, you see this same thing expressed differently

C++
template <class T> function_taking_any_length(LENGTH::unit<T> arg);

 

Conversions

The is_conv_factor and to_base_factor members of the description class provide the information needed to determine if a conversion is necessary (none will be compiled if not) and what it should be. This is done using the following mechanism.

C++
template<class T, bool>  struct convert_to_base_imp_s
{
	inline double convert(double value)
	{
		return  T::to_base_factor * value;
	}
}
template<class T>  struct convert_to_base_imp_s<false>
{
	inline double convert(double value)
	{
		return  value;
	}
}
template <class T> double convert_to_base(double value)
{
	convert_to_base_imp_s<T, T::is_conv_factor>::convert( value);	
}

both versions of convert will compile inline but the version in struct convert_to_base_imp_s<T, false> does nothing and therefore the convert_to_base that calls it does nothing. As a result, their inline compilation will be nothing, as if the calls never existed. In this way run-time conversion code is eliminated completely wherever it is not needed.

This neatly encapsulates conversions between scaled units and the base units that they are scaled from and by implication conversions between different units scaled from the same base units. However this library supports multiple rational unit systems and that is not quite so simple.

When multiple rational unit systems are supported, a conversion from one unit to another can involve:

  • a conversion from a scaled unit to its base,
  • a conversion from one rational rational unit system to another that may have components in several dimensions
  • and a conversion from base units of that rational unit system to scaled units.

These three components are managed by the template struct unit_to_unit<>

C++
template <class UFrom, class UTo> struct unit_to_unit:

unit_to_unit<> holds only the compile time constants is_conv_factor and factor:

C++
static constexpr double factor = 
	UFrom::to_base::factor
	* Sys2Sys<
		UFrom::dimensions, UFrom::unit_sys, UTo::unit_sys
	>::factor
	/ UTo::to_base::factor;

enum {
	is_conv_factor = (1.0== factor)? 0 :
		(std::is_same<ufrom, uto="">::value != 0) ? 0
		: UFrom::to_base::is_conv_factor
		+ Sys2Sys<
            UFrom::dimensions, UFrom::unit_sys, UTo::unit_sys <ufrom::dimensions, ufrom::unit_sys="">
		>::is_conv_factor
		+ UTo::to_base::is_conv_factor
	};
</ufrom::dimensions,></ufrom,>

Both constants chain together the three conversion components described above encoded as class to_base defined for each unit and the class Sys2Sys<UFrom::dimensions, UFrom::unit_sys, Uto::unit_sys> which I will describe shortly. Each of these also hold is_conv_factor and factor constants themselves.

The factor constant is formed as you would expect from the factor members of the three components.

The is_conv_factor however is formed by adding together the is_conv_factor members of the three components. If any of the the components have a non zero is_conv_factor then the resultant is_conv_factor will be non-zero, indicating that factor must be used to carry out a conversion. However if they are all zero then the resultant is_conv_factor will be zero indicating that no conversion is required. This evaulation is bypassed and is_conv_factor set to zero if:

  • factor evaluates to 1.0 (1.0== factor)? 0 : although not reliable as a primary mode of operation, this test can in practice achieve some cancellation of factors.
    or Ufrom and Uto are the same (std::is_same<ufrom, uto="">::value != 0 ? 0 :</ufrom,>.

In the code you will find an additional indirection of the factor const which ensures that once a zero is_conv_factor is established, its factor member will always be read as exactly 1.0.

C++
static constexpr double factor = std::conditional_t
	<
		is_conv_factor != 0,
		with_factor,
		no_factor_base
	>::factor;

This ensures that error creep in the normal evaluation of factor does not pollute its identity as the identity factor.

The template struct Sy2Sys<> deals with conversions between one rational unit system and another for any dimension of measurement (LENGTH, TIME, VELOCITY, Etc.). Its template parameters are: a dimensions struct and two integers representing the two rational unit systems.

C++
template <class dims, int from_sys, int to_sys> struct Sys2Sys;

A specialisation of Sys2Sys<> is defined to deal with its application to conversions between a rational unit system and itself as a non conversion.

C++
template <class dims, int Sys> struct Sys2Sys<dims, Sys, Sys>
			: public no_factor_base
		{};

While there is only one rational unit system, this will be the only specialisation needed and its role in the implementation of unit_to_unit<> is to indicate a factor of 1.0 and no conversion. So that the unit_to_unit factor evaluation will compile as if written:

C++
static constexpr double factor =	Ufrom::to_base::factor / UTo::to_base::factor;

The general implementation of Sys2Sys<> defines its factor constant as

C++
static constexpr double factor = 
	#define ULIB_ENUMERATED_TERM(n)	\
	(					\
		(dims::D##n != 0) ?		\
			int_power<dims::D##n>	\
				::of(Base2Base4Dim<n, from_sys="">::factor)\
	: 1)
	ULIB_CONCAT_ENUMERATED_TERMS_FOR_ALL_DIMS(*)
	#undef ULIB_ENUMERATED_TERM	
	;
</n,>

which expands for 5 dimensions as:

C++
static constexpr double factor = 
	((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys="">::factor): 1);
</n,>

Base2Base4Dim<> represent the conversions between the base dimensions (those such as LENGTH, MASS, TIME defined using ulib_Base_dimension$ macro) for two rational unit systems.

Sys2Sys<> builds up a compound conversion from these as indicated by the dimensions<> struct passed to it. Its template parameters are an integer representing a base dimension and two intergers representing the two rational unit systems:

C++
template <int Dimension, int from_sys, int to_sys>
		struct Base2Base4Dim;

A specialisation of Base2Base4Dim<> is defined to deal with its application to conversions between a rational unit system and itself as a non conversion.

C++
template <int Dimension, int Sys>
		struct Base2Base4Dim<Dimension, Sys, Sys>
			: public no_factor_base
		{};

and its general implementation is

C++
template <int Dimension, int from_sys, int to_sys>
struct Base2Base4Dim
{
	static constexpr double factor =
		Base2Base4Dim<Dimension, from_sys, 1>::factor
		*Base2Base4Dim<Dimension, 1, to_sys>::factor;
	enum {
		is_conv_factor = (1.0 == factor)? 0 :
		Base2Base4Dim<Dimension, from_sys, 1>::is_conv_factor
		+ Base2Base4Dim<Dimension, 1, to_sys>::is_conv_factor
};

which relies on specialisations of Base2Base4Dim<> that are defined when you define a secondary base unit. For example when you write

C++
ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)

The following specialisations of Base2Base4Dim<> are generated:

C++
template <>		
struct ulib::ung::factors::Base2Base4Dim<1, 2, 1>	
{	
	enum { is_conv_factor = 1 };
	static constexpr double factor = 0.01;
};	
template <>	
struct ulib::ung::factors::Base2Base4Dim<1, 1, 2>
{
	enum { is_conv_factor = 1 };
	static constexpr double factor = 100;
};	

The first template parameter of 1 represents the base dimension of LENGTH and the other two represent the two rational base systems. A Base2Base4Dim<> is generated for each direction of conversion.

Similarly when you write

C++
ulib_Secondary_base_unit(cgs, grams, =, 0.001, Kgs)

The following specialisations of Base2Base4Dim<> are generated:

C++
template <>		
struct ulib::ung::factors::Base2Base4Dim<2, 2, 1>	
{	
	enum { is_conv_factor = 1 };
	static constexpr double factor = 0.001;
};	
template <>	
struct ulib::ung::factors::Base2Base4Dim<2, 1, 2>
{
	enum { is_conv_factor = 1 };
	static constexpr double factor = 1000;
};	

In this case the first template parameter of 2 represents the base dimension of MASS.

When you write

C++
ulib_Secondary_base_adopt_base_unit(cgs, secs)

The following is generated

C++
template <>		
struct ulib::ung::factors::Base2Base4Dim<3, 2, 1>	
{	
	enum { is_conv_factor = 0 };
	static constexpr double factor = 1;
};	
template <>	
struct ulib::ung::factors::Base2Base4Dim<3, 1, 2>
{
	enum { is_conv_factor = 0 };
	static constexpr double factor = 1.0;
};

In this case the first template parameter of 3 represents the base dimension of TIME and is_conv_factor = 0 and factor = 1.0 indicate that there is an identity between MKS and cgs in the dimension of TIME. They both use secs.

We now have all the components necessary to implement a Sys2Sys<> struct for conversions between MKS (1) and cgs (2) for any of the mechanical dimensions of measurement. For instance, for FORCE which is dimensions<1, 1, -2, ...>, the following Sys2Sys<> struct will be instantiated

C++
template <> struct Sys2Sys<FORCE, 1, 2>;

with its factor member constructed from the Base2Base4Dim<> specialisations that we have defined.

C++
static constexpr double factor = 
	int_power<1>::of(Base2Base4Dim<1, 1, 2>::factor)
	* int_power<1>::of(Base2Base4Dim<2, 1, 2>::factor)
	* int_power<-2>::of(	Base2Base4Dim<3, 1, 2>::factor);

and in the same manner the Sys2Sys<> struct for conversion in the opposite direction

C++
template <> struct Sys2Sys<FORCE, 2, 1>;

All of this calculation of unit_to_unit<> takes place during compilation. The result will be to insert a single factor multiplication in the run-time code to carry out the conversion, or to insert nothing. This is done by using the same pattern as  at the convert_to_base<>() function described above.

C++
        template<class To>
        struct convert_to 
        {
        private:
            template<bool, class From> struct no_conv_factor
            {
                static constexpr double convert(From const& from)
                {
                    return from.Value();
                }
            };
            template<class From> struct no_conv_factor<false, From>
            {
                static constexpr double convert(From const& from)
                {
                    return factors::unit_to_unit<From, To>::factor 
                                                        * from.Value();
                }
            };
        public:
            template<class From>
            static constexpr double from(From const& from)
            {
                return no_conv_factor
                    <
                    false == factors::unit_to_unit<From, To>::is_conv_factor,
                    From
                    >::convert(from);
            }
        };

//called as follows
convert_to<metres>::from(2.5_Kms);

 

General comments

There is a lot more detail to how it works but I think that with the orientation above you can work it out by examining the code.

I didn't write this out and get it right first time. It was a process of re-engineering and re-factoring the code of the original library with incremental changes which at all times kept it working and testable. Neither did I get the design right first time. Many times I had to back track on previous design decisions, even at late stages of development.

I have done my best to make it optimally run-time efficient and believe I have succeeded. This has been achieved by pre-calculating everything that is known at compile time during compilation. This, along with the dimensional analysis means that the compiler will have to work harder than it would have to using raw numbers instead of unit typed quantities.

The C++ language is only just waking up to its own ability for compile time programming that accidentally appeared with the introduction of templates.
constexpr symbols, constexpr functions and template using are the beginnings of an acknowledgement of this by the language standard.

C++ 11 facilitates being able to do all the compile time programming necessary but not always in the most efficient way that you might imagine. Furthermore when balancing readability against compile time efficiency, I have chosen readability. Nevertheless the compile time burden is linear in nature and has no exponential processes or deep recursions that might choke the compiler. It does make sense to get the compiler to do some work for you – checking correctness and pre-calculating all required conversions. It is what it is for.

C++14 starts to open the doors to compile time programming that is both more readable and more efficient to compile – for instance by extending what can be done within a constexpr qualified function. Applying this, which this library has not done, could produce some improvement in compilation efficiency but I believe it would be marginal. C++ 11 was the great enabling watershed and there is no pressing reason to rewrite it for C++14.

There remain some hoops I had to jump through because the language wouldn't let me do what I wanted.

  • Floating point numbers still can't be template parameters. I have already describe how it was necessary to write out a class on the fly with a constexpr static double member initialised literally to work around this.
  • To build a linked list of units for combo initialisation I had to resort to template function specialisation based on line numbers. A compile time variable, or even just a counter would remove the need for such sophisticated and opaque constructions. It can't be that difficult to provide it.

I have taken create care with writing this library but nevertheless have found myself rooting out bugs and typos even during the last stages of development and testing. I hope that it is complete and working. I do feel confident that any troublesome corner cases that have remained hidden can be resolved quickly once discovered. I have already heaved the architecture through major refactorings and fundamental design changes. It is not brittle.

History

This is the first release of this library for C++11. It is a development from Units of Measurement types in C++ pubished on The Code Project in September 2014 which remains available for pre C++11 compilers.

License

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


Written By
Retired
Spain Spain
Software Author with engineering, science and mathematical background.

Many years using C++ to develop responsive visualisations of fine grained dynamic information largely in the fields of public transport and supply logistics. Currently interested in what can be done to make the use of C++ cleaner, safer, and more comfortable.

Comments and Discussions

 
QuestionIntelliSense doesn't understand some operators Pin
mtwombley30-Oct-21 21:47
mtwombley30-Oct-21 21:47 
QuestionHow can I use std::is_base_of? Pin
mtwombley30-Oct-21 9:10
mtwombley30-Oct-21 9:10 
QuestionNamespace usage breaks 10.5_cm expressions Pin
mtwombley30-Oct-21 7:08
mtwombley30-Oct-21 7:08 
BugDoesn't compile with new VS /permissive- Pin
mtwombley30-Oct-21 6:40
mtwombley30-Oct-21 6:40 
QuestionCannot Compile on OSX Pin
Member 1093613316-Sep-17 4:40
Member 1093613316-Sep-17 4:40 
AnswerRe: Cannot Compile on OSX Pin
john morrison leon16-Sep-17 23:01
john morrison leon16-Sep-17 23:01 
GeneralRe: Cannot Compile on OSX Pin
Member 1093613317-Sep-17 11:35
Member 1093613317-Sep-17 11:35 
GeneralRe: Cannot Compile on OSX Pin
john morrison leon17-Sep-17 23:50
john morrison leon17-Sep-17 23:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.