Quantity Library - Rationale

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


Q: Why does the library leave some things out that would be useful and could be done in ISO C++? Why does it use such odd techniques to implement the features it does provide?

A: Because this library gives more importance to portability than to power and implementation elegance.

Given that few if any current C++ compilers are fully ISO-compliant, any author who wants his library to see actual use has to make some compromises. The more features of the language you use, and the harder you push them, the fewer users you will have -- yet the fewer features of the language you use, the less powerful your library can be. As is the case with most engineering decisions, you have to make a compromise.

In the case of the Quantity library, I've chosen to give much more weight to portability and robustness than to power and elegance of implementation. I want this library to be usable by as many C++ programmers as possible, and I don't want to deal with code that gets brittle when it's ported to compilers that I don't have access to. In this particular case the choice is fairly easy to make, because Fermilab's SIunits library already occupies the other end of the trade-off spectrum; it's very powerful but isn't terribly useful under MSVC (technically it compiles, but it loses all its static type-checking, which is the whole reason for having such a library in the first place.)

Basically I decided to maximize portability by avoiding:

There is no question that this decision made some of the code ugly. In a similar context Ed Brey remarked that trade-offs like this are "a lot of ground to give to appease a few broken compilers." I completely agree. It is a lot of ground to give, it made my job more difficult, and it led to leaving out functionality that I wanted to include.


Q: Why are member functions defined in-class?

A: Some compilers have trouble with out-of-class template member definitions, so for portability I had to put them in-class. For consistency I chose to put all member definitions in-class. This uglifies the header file, but it doesn't corrupt the actual user interface to the library.


Q: Why do you use that hokey "permit" class?

A: If the library just declared everything that uses "permit" as a friend of all instantiations of quantity<>, then we could remove the permit class completely.

However, many of these functions are templated and template friend declarations seems to be a weak spot with some compilers, so I decided not to use them at all. Unfortunately I needed all instantiations of the quantity<> class to have access to all other instantiations, so I used the "quantity::detail::permit" class as a work-around. Technically this allows any code to access the internal representation of a quantity<>, but to do so you have to reach into the detail namespace, and doing that in user code should raise the same red flag that reinterpret_cast<> would. The important point is that accidental non-type-safe access is prevented.

This is an imperfect solution, but it allows a large class of programmers to use the library who would otherwise be unable to do so.

By the way, I also tried two other techniques to avoid the template friend problem:

Overall, the simplicity and uniformity of the permit class wins.

Q: Why do you use a macro to control the numerical representation?

A: Ideally the type used to represent the numerical value inside a quantity<> should be a template parameter. However, when I tried to implement this the operators got very confusing and I was afraid that even if I got things working the code would be brittle. Therefore I chose to design the library so that all quantity<> objects use the same representation type. A macro is provided to allow the user to change that type if needed, with clear documentation about the risks of violating the One Definition Rule. Although grossly inelegant, this should let users do what they need to do with minimal complexity and maximum portability.


Q: Why did you choose the name "quantity" for the main class (and the library itself, the namespace, and the main header file)?

A: The term "quantity" was chosen to be consistent with National Institute of Standards and Technology Special Publication 811 (SP811), available at http://physics.nist.gov/Document/sp811.pdf


Q: Why does the library predefine common dimension<> types instead of the corresponding quantity<> types?

A: The library uses

	typedef dimensions< 0, 1, 0 > mass_d
rather than
	typedef quantity< dimensions< 0, 1, 0 > > mass_t
This makes user code slightly more verbose, but it has two advantages: Note that if this extension is ever implemented, then something like "mass" truly will be a dimension, and there could be different actual types (float, double, long double) implementing it. You want a single typedef for the concept of "mass" to apply to all of them. (This may be speculative generality, but I thought it looked more elegant this way anyway.)

Q: Why is dimensions< 0, 0, 0 > not used for dimensionless quantities?

A: I tried it both ways, and using a type-generator to make dimensionless results come out as primitive types was a lot easier to get working. If you use dimensions< 0, 0, 0 >, then you have to use a bunch of implicit conversion stuff to make them act like regular numbers. Implicit conversions are often just trouble waiting to happen, so biting the bullet and forcing dimensionless results of operations to be numbers in the first place ends up being simpler and more obvious.


Q: Why are units like meter() and constants like pi() and nano() defined as inline functions instead of as const data?

A: Two reasons:

Of course, the syntax isn't as pretty, but I decided it was worth it for the correctness, simplicity, and efficiency.

Q: Why are there no dimensions for plane_angle and solid_angle? Things like degree_angle(), radian(), and steradian() can be confused with each other and with plain numbers.

A: According to SP811, plane angle and solid angle are dimensionless, and the quantity package follows that definition. This is not a unique case; there are at least two other pairs of units that can be confused: hertz()/becquerel() and gray()/sievert().

This does allow confusion, but to keep complexity within reasonable bounds, the current version of the quantity library is designed to check dimensions only, and it cannot detect conversions between different quantities which have the same dimensions. The units radian(), degree_angle() etc. are defined for convenience and clarity, but the library will not detect incorrect usage. (Future extensions that would do so are being considered, but such extensions will be implemented only if it can be done consistently with the official standard described in SP811.)


Q: Why is there no operator% (modulo)?

A: The user can easily define one that would be just as efficient as anything the library could provide. Since I didn't expect it to be very useful anyway, I left it out to reduce feature creep.


Q: Why did you use "speed_d" instead of "velocity_d"?

A: In many contexts, "velocity" is used to mean a vector that includes both magnitude and direction. "Speed" is less ambiguous, since it always represents just a magnitude. If you really want the velocity, you need to use a vector< quantity< speed_d > >.


Q: Why do you use the name mile_nautical instead of nautical_mile, ounce_troy instead of troy_ounce, etc.

A: It is always perilous to contradict common usage, but it is amazing how many groups of units there are that have similar names, and to avoid confusing the user I felt it was best to pick a single convention and be totally consistent. Thus this library always puts the common part of the name first:

and my personal favorite, the eight different tons: (I know that's actually nine, but one is just a spelling difference.)

If it makes it any easier, just pretend you're in the military and read the underscore as a comma where appropriate: "ounce, fluid, imperial". If a particular name really bugs you, it's easy enough to define your own wrapper with a different name.


Q: What is the funny macro definition of "pascal()" for?

A: Some compilers consider "pascal" to be a keyword, which really confuses things if you try to define an inline function called pascal(). Therefore, on compilers that have this problem, we define a macro that changes pascal() to pascal_(). This affects both the definition of the function and the invocations of it, so everything stays in synch.

This obviously avoids the problem, but what's not so obvious is that it also still allows use of the pascal keyword -- the macro is defined to take arguments, so only occurrences of the word "pascal" followed by a left paren are taken to be macro invocations and expanded; anywhere the word "pascal" stands on its own (not followed by parentheses) it will still be treated as a keyword.

Thanks to Darin Adler for devising this nifty work-around.


Last updated 2001-07-11.