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.
Introduction, Background,
Overview,
Using the code,
The example application,
Quick Reference, How it works, History
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.:
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):
LENGTH d = 50*metres; TIME t = 2*secs; 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:
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.
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)ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)ulib_Compound_unit(Joules, =, Newtons, Multiply, metres)ulib_Compound_unit(Watts, =, Joules, Divide, secs)
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
constexpr metres dist = 200_metres; constexpr secs t = 2_secs; constexpr metres_psec velocity = dist / t;
-
It provides an optional tolerance operator ^
for use with comparison and equality tests
if(length1 == length2 ^ 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
sq_metres Area = 3.14159 * 2000_metres * 2000_metres;
will be evaluated in metres
to produce a result in sq_metres
, and
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.
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.
template<class T>
auto area_of_circle(LENGTH::unit<T> radius) {
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.
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
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.
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:
metres dist = metres(10); metres dist = secs(10);
but if you want it to properly embrace more intelligent use of units then you also have to enforce:
metres_psec velocity = metres(10) / secs(2); metres_psec velocity = metres(10) * secs(2);
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.
template<int Length, int Mass, int Time> class unit
{
enum {length=Length, mass=Mass, time= Time};
double value; ....
};
so we can define the fundametal dimensions of measurement
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
using VELOCITY = unit<1, 0, -1>; using ACCERATION = unit<1, 0, -2>; using FORCE = unit<1, 1, -2>;
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:
LENGTH metres(1);
LENGTH Kms(1000);
TIME secs(1);
TIME hours(3600);
and you can write
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.
The basic principle is :
Define some units that you want to use:
#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:
metres width = 10_metres; sq_metres area = 20000_sq_metres;
Kms length = area / width; secs transit_time = 40_secs;
Kms_phour velocity = length / transit_time;
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:
ulib_Compound_unit(metres_psec, =, metres, Divide, secs)
ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)
ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)
ulib_Compound_unit(Joules, =, Newtons, Multiply, metres)
ulib_Compound_unit(Watts, =, Joules, Divide, secs)
You begin by defining some fundamental dimensions of measurement
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
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:
ulib_Unit_as_square_of(sq_metres, metres) ulib_Unit_as_cube_of(cubic_metres, metres)
ulib_Scaled_unit(Kms, =, 1000, metres) ulib_Scaled_unit(mins, =, 60, secs) ulib_Scaled_unit(hours, =, 60, mins)
ulib_Unit_as_inverse_of(Herz, secs)
ulib_Compound_unit(metres_psec, =, metres, Divide, secs) ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs) ulib_Compound_unit(Kms_psec, =, Kms, Divide, secs)
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.
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
ulib_Secondary_rational_unit_systems(cgs)
and declare cms
and grams
as base units of it
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.
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
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
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
ulib_Secondary_rational_unit_systems(cgs, KmsMins)
and then define Kms
and mins
as members of it
ulib_Secondary_base_unit(KmsMins, Kms, =, 1000, metres)
ulib_Secondary_base_unit(KmsMins, mins, =, 60, secs)
allow it to adopt Kgs
ulib_Secondary_base_adopt_base_unit(KmsMins, Kgs)
and define a rational unit of velocity to go with it
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.
Kms_pmin v = 40_Kms / 5_mins;
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:
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
.
Once you have defined a unit you can declare quantities in that unit:
metres len;
You can intialise it by explicitly passing a numerical value in the constructor
metres len(4.5);
but otherwidse it can only be initialised or assigned by a compatible unit typed quantity (one that measures the same thing)
metres len = 4.5_metres; len = 1.2_Kms;
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
len = 1200;
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.
metres length = 200_metres;
double len_in_metres = as_num_in<metres>(length); double len_in_Kms = as_num_in<Kms>(length);
double try_this = as_num_in<secs>(length);
There are some type modifiers that can be applied to the units you have defined as follows:
metres::squared area;
metres::cubed volume;
metres::to_power_of<4> variance_of_area;
sq_metres::int_root<2> side_of_square;
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:
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.
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 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
Kms total_length = 2_Kms + 3* 5_Kms + 4_Kms;
or a comparison
bool is_bigger = 5_Kms > 3_Kms;
will be evaluated entirely in Kms
without any conversions and so will this
sq_Kms Area = 3.14159 * 5_Kms* 5_Kms;
but the following
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.
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 <
and >
if 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:
if( distance1 == distance2 ^ 5_cms) 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.
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
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
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
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
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.
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
ulib_Base_dimension_4(TEMPERATURE)
and define degrees_C
and degrees_F
as base and Scaled units for that dimension
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.
ulib_Name_common_datum_point_for_dimension(TEMPERATURE, water_freezes) ulib_datum_Measurement(Celcius, degrees_C, 0, @, water_freezes) ulib_datum_Measurement(Fahrenheit, degrees_F, 32, @, 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:
Celcius T1 = 32_Fahrenheit;
T1 += 10_degrees_C;
Fahrenheit T2 = T1 - 32_degrees_F;
degrees_C temp_diff = T1 - T2;
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.:
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
degrees_C arg = 293_Kelvin;
The underlying units can also be converted to an absolute measurement but it must be done explicitly
Kelvin absTemp = Kelvin(293_degrees_C);
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.
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:
metres radius = 5_metres;
sq_metres area_of = 3.14159 * radius* radius;
you will also want to be able to write:
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
.
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
sq_metres area = sq_metres(area_of_circle(as_num_in<metres>(radius));
Instead we simply type the function using unit types
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:
sq_metres area = area_of_circle(5_metres);
You can also use it with other units that measure length
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:
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.
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:
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:
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:
template<class T>
auto area_of_circle(LENGTH::unit<T> radius) {
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
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:
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:
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
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
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.
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 double
s.
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:
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:
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 ,
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.
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:
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
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
return degrees_C(
get_max_difference_between_three_numbers(
as_num_in<TEMPERATURE::measure<T1>>(t1),
as_num_in<TEMPERATURE::measure<T2>>(t2), as_num_in<TEMPERATURE::measure<T3>>(t3)) );
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:
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;
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:
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:
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.
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.
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
metres len = sqrt(20_sq_metres);
metres len = int_root<3>(20_cubic_metres);
auto res = sqrt(5_metres);
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.
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
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
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.
Celcius ave_temp = mean_of(t1, t2, t3);
and for good measure, variadic functions for variance and standard deviation
degrees_C::squared variance = variance_of(t1, t2, t3);
degrees_C deviation = std_deviation_of(t1, t2, t3);
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.
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.
ulib::for_each_compatible_unit<ref_unit>
(
[this](
char* szName,
run_time_conversion_info<ref_unit> converter,
bool is_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
double to_display_units(ref_unit const& u)
which you should use to write the quantity referenced in your code to the screen
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.
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:
#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:
#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
#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:
#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
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
ulib_Secondary_rational_unit_systems(cgs, Kmsmins)
this can be omitted if there aren't any
Begin the unit definitions
ulib_Begin_unit_definitions
- define your units beginning with the base units
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
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
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 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 _tWinMain
. You can remove thw .rc and resource.h files from your project, they will not be used.
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.
ulib::for_each_compatible_unit<ref_unit>
(
[this](
char* szName,
run_time_conversion_info<ref_unit> converter,
bool is_ref_unit
)
{
cmbUnit.do_msg(CB_INSERTSTRING, 0, static_cast<wchar_t *>(wchar_buf(szName)));
converters.insert(converters.begin(), converter);
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.
std::vector<run_time_conversion_info<ref_unit>> converters;
to be used when writing to and reading from the display
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.
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)
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)
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).
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.
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.
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.
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
ulib_Base_dimension5(ANGLE);
and a primary base unit of radians
ulib_Base_unit(radians, ANGLE)
and our definition of radial_metres
ulib_Compound_unit(radial_metres, =, metres, Divide, radians)
Now we can properly define Torque
ulib_Compound_unit(Nm_torque, =, Newtons, Multiply, radial_metres)
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
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
ULIB_MEASUREMENT_CTRL(edtPower_output, under<_edtRotational_velocity>,
0, BWidth * 3 / 2, Watts)
Which is calculated as the product of the two input quantities.
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.
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.
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.
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.
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
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
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
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
uulib_Secondary_base_adopt_base_unit(cgs, secs)
|
Datum measure definition macros
see Measurements against a datum ulib_Name_common_datum_point_fo r(dimension, name_of_datum_point)
name_of_datum_point should be a descriptive identifier
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
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
ulib_Absolute_measurement(Kelvin, degrees_C, 273, @, water_freezes)
|
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.
double numerical_value_in_Kms = as_num_in<Kms>(5500_metres);
|
abs(quantity) returns the absolute positive value
metres absolute_value = abs(-5_metres);
|
powers and roots - see Maths functions
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);
metres edge_of_cube = integer_root<3>(625_cubic_metres)
|
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.
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
Celcius average_temperature = mean_of(120_Celcius, 230_Fahrenheit, 300_Kelvin );
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.
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.
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.
LENGTH::rational_unit<working_sys>;
VELOCITY::rational_unit<working_sys>;
|
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
template <int d1, int d2, int d3 .....>
struct dimensions
{
enum {D1 = d1, D2 = d2, D3 = d3 .....};
….......
}
When you define a base dimension
ulib_Base_dimension1(LENGTH)
you define LENGTH
as a specialisation of dimensions<>
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:
template <
class T, 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
ulib_Base_unit(metres, LENGTH)
the following code is generated
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
ulib_Scaled_unit(feet, =, 0.3048, metres)
the following code is generated
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.
template <class T> function_taking_any_length(ulib::unit<T, LENGTH> arg);
at the user level, you see this same thing expressed differently
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.
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<>
template <class UFrom, class UTo> struct unit_to_unit:
unit_to_unit<>
holds only the compile time constants is_conv_factor
and factor
:
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.
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.
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.
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:
static constexpr double factor = Ufrom::to_base::factor / UTo::to_base::factor;
The general implementation of Sys2Sys<>
defines its factor
constant as
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:
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:
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.
template <int Dimension, int Sys>
struct Base2Base4Dim<Dimension, Sys, Sys>
: public no_factor_base
{};
and its general implementation is
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
ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)
The following specialisations of Base2Base4Dim<>
are generated:
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
ulib_Secondary_base_unit(cgs, grams, =, 0.001, Kgs)
The following specialisations of Base2Base4Dim<>
are generated:
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
ulib_Secondary_base_adopt_base_unit(cgs, secs)
The following is generated
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
template <> struct Sys2Sys<FORCE, 1, 2>;
with its factor
member constructed from the Base2Base4Dim<>
specialisations that we have defined.
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
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.
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);
}
};
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.
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.