The new header <limits> can do a lot for you if you use it, and if you supplement it a bit.
Introduction
In numerical applications a program sometimes needs access to some extra information about a numerical variable, such as its maximum size, or whether or not it is a signed type. As an example, suppose I wish to use the native types for manipulating data in my code, but want to store the data in the smallest possible variable on disk. In my "C" days I would have written a bit of code like this:
#include <limits.h> FooFunction(int value) { short index; assert(SHRT_MIN <= value && value <= SHORT_MAX); index = value; ... }The assertion ensures that the integer stored in value (an int) fits in a short variable. This code accesses the native size type definition found in the C header <limits.h>. The C++ Standard header <limits> [1] expands and extends the information available to programers from the C header <limits.h>.
Using <limits> and the standard class numeric_limits defined within, my code now looks like this:
#include <limits> FooFunction(int ivalue) { short index; assert(numeric_limits<short>::min() <= ivalue && ivalue <= numeric_limits<short>::max()); index = ivalue; ... }Again, this code ensures that the value contained in an int (in this case, ivalue) will fit inside a short.
What I really want is to know that whatever the types of ivalue and index, index will hold the contents of ivalue without overflow. I'd like to be able to verify this without having to explicitly specify the type of index. With some simple templates we can ask the variables themselves for this information.
Template Argument Deduction
Using a bit of template magic, I wrote a bunch of forwarding functions for the numeric_limits class. They are all very simple, one-line functions like this one for max:
template <class T> inline T max(T const &) throw() { return std::numeric_limits<T>::max(); }The compiler can look at this template function's argument and deduce the argument or type at compile time. Now I can write:
FooFunction(int value) { short index; assert(min(valve) <= valve && valve <= max(valve)); index = value; }The compiler sees the function call to max, and knows that a template function max has already been defined, so it uses the type of index to instantiate the function short max<short>(short const &). (The same process applies to min.) The instantiated function is equivalent to:
short max(short &) { return std::numeric_limits<short>::max(); }Examination of the <limits> header reveals that the specialization for numeric_limits<short>::max returns SHRT_MAX! It's the same test as performed in the old C code, after all the inlining goes away. (By the way, this technique of using templates to deduce types is known as the pattern "Automatically Detect Types," by Paul Jakubik [2]. It's quite handy.)
Avoiding Pollution
To avoid polluting the global namespace, and to avoid conflicts with other library code, I've put these functions in the namespace xlimits, the "x" denoting "extended limits." (The full source code is available on the CUJ ftp site. See p. 3 for downloading instructions.)
Now I either hoist the xlimits names to the global namespace:
using namespace xlimits;and make my calls to max the same as above; or I fully specify the function like this:
FooFunction(int value) { short index; assert(value <= xlimits::max(index) && xlimits::min(index) <= value); }I tend to favor writing it out in full because "max" has been in my lexicon as shorthand for the "maximum" of two values so long it's almost engraved in my head. When I see a call to max with just one argument I start wondering what the compiler has done to me now. With the namespace qualifier I instantly remember why I'm calling this version of max and what it does.
Not Just Syntactic Sugar
This may seem like a lot of sugar coating on some very simple code that should be hard to get wrong. I agree. Its benefit becomes apparent when it's time to do maintainence. Thanks to numeric_limits, the test for the variable's maximum size depends only on its type. But without the use of xlimits::max, that type must be explicitly specified in two places within the code, in the definition of the variable and in the test. Now it is specified in just one place, the definition. If some future programmer changes its type, the test will change automatically to use the correct new limit value.
Now before you just toss this whole business of using numeric_limits as being excess baggage, I would like to point out why numeric_limits is a template, instead of a series of #defines, and how that benefits C++ programmers.
When I create my own data types (and I have several) I set up numeric_limits to know about it. For example, I use fixed-point math [3] occasionally so I have some fixed-point classes that do the math for me. Like the built-in numeric types, fixed-point types have maximum values. To make these classes easy for other team members to use, I have created explicit specializations of the numeric_limits class. Given a fixed-point type Fixed10Dot6, a call to numeric_limits<Fixed10Dot6>::max() returns the correct maximum value. Now look back at my call to the xlimits::max function it works with this new data type just as well as before:
Fixed10Dot6 FooFunction(Fixed10Dot6 value) { double x; // use native register type. // do some calculations with x that I'm "sure" // will fit in a Fixed10Dot6 // Lets test just because. assert(xlimits::min(valve) <= x && x <= xlimits::max(valve)); Fixed10Dot6 result(x); return x; }This all works because numeric_limits is a template. And a template function can have different implementations for a given specialization. The header <limits> defines specializations of numeric_limits for all the native types, including int, float, etc. Designers of new data types can hook into this mechanism by providing their own specializations. This is a very powerful tool.
I also have a constructor for a Fixed10Dot6(double) and a friend bool operator<(double, Fixed10Dot6), so that the comparison above works. These additional operators are part of the Fixed10Dot6 class, which I needed anyway for mixed math.
The rest of the functions in numeric_limits can be specialized in similar fashion for a user-defined type such as Fixed10Dot6.
Conclusion
Using the Standard library header <limits>, with a little help from some forwarding functions, it is simple to generate type-safe code that is very readable and easy to maintain.
Notes
[1] P.J. Plauger wrote a very nice description of the new <limits> header in "Standard C/C++: The Header limits," CUJ, September 1997. I refer you to that article for descriptions of the individual numeric_limits functions available.
[2] See Paul Jakubik's "Callback Implementations in C++" at http://www.primenet.com/~jakubik/callback.html
[3] Fixed-point numbers are a way of representing real numbers using an int, or a short, or even a char. C++ doesn't automatically shift the decimal point in fixed-point numbers, since they aren't built-in types; the programmer must keep track of it herself, or encapsulate the shifting operations within a class. Fixed-point numbers are often used on machines without a floating-point processer, such as some embedded systems, or old 486s. My fixed-point classes help me out by correctly shifting the decimal point before adding a fixed-point number to constants and reminding me to shift-adjust two different-sized fixed-point numbers before manipulating them.
Gary Powell Is a Senior Software Engineer at Sierra On-Line. He can be reached at gary.powell@sierra.com.