I recently received an email regarding an article from an Overload published way back in 2001. It is wonderful to hear people are still looking back through items people have written, no matter how long ago. The email drew my attention to C++11’s
std::numeric_limits<T>::lowest()
, which I hadn’t come across so I thought others might be in the same situation. Lion has kindly taken the time to formalise his initial observations into the following letter, and I have included a response from the original author, Thaddaeus Frogley.
Dear Editor,
I have just read the
Overload
article ‘An introduction to C++ Traits’ [
Frogley01
] and noticed that the initialisation of
largest
in the first code fragment (the one adapted from [Veldhuizen]):
T largest = std::numeric_limits< T >::min();
is done using
std::numeric_limits<T>::min()
where it could now be C++11’s
std::numeric_limits<T>::lowest()
for cases where the type
T
is float or double and all array members are below or equal to zero.
Although I couldn’t find an explicit reference, this seems to originate from C where
INT_MIN
and
FLT_MIN
or
DBL_MIN
show the same semantic difference; some people guess that it is because the libraries for rational and for integral numbers have been developed by different groups. Fernando Cacciola explains it very nicely in the N1880 proposal to the C++ standard [
N1880
]:
numeric_limits::min() (18.2.1.2) is defined with a meaning which is inconsistent across integer and floating-point types. Specifically, for integer types, it is the minimum finite value whereas for floating point types it is the minimum positive normalized value. The inconsistency here lies in the interpretation of minimum: in the case of integer types, it signifies lowest, while for floating point types, it signifies smallest non-zero.
N2348 adds some history [ N2348 ]:
At Mont Tremblant, Pete Becker noted that the wording was flawed, because not all floating point representations have the feature that the most negative representable finite value is the negative of the most positive representable finite value.
An example (Listing 1) shows the different behaviour.
#include <limits> #include <iostream> template< class T > T findMax(const T * data, const size_t numItems) { // Obtain the minimum value for type T T largest = std::numeric_limits< T >::min(); for (unsigned int i = 0; i<numItems; ++i) if (data[i] > largest) largest = data[i]; return largest; } template< class T > T findMaxNew(const T * data, const size_t numItems) { // Obtain the minimum value for type T T largest = std::numeric_limits< T >::lowest(); for (unsigned int i = 0; i<numItems; ++i) if (data[i] > largest) largest = data[i]; return largest; } template< class T > void printAry(const T * data, const size_t numItems) { for (unsigned int i = 0; i<numItems; ++i) std::cout << data[i] << " "; std::cout << std::endl; } void test(float* ary, size_t nof) { printAry(ary, nof); float maxFloat = findMax<float>(ary, nof); float newMaxFloat = findMaxNew<float>(ary, nof); std::cout << "max in only negative floats is " << maxFloat; if (std::abs(maxFloat - newMaxFloat) > std::numeric_limits<float>::epsilon()) std::cout << " but should have been " << newMaxFloat; std::cout << std::endl << std::endl; } int main() { float onlyNegativeFloats[] = { -2, -1, -3 }; std::cout << "array of only negative floats: "; test(onlyNegativeFloats, 3); float positiveAndNegativeFloats[] = { 2, -1, -3 }; std::cout << "array of positive and negative floats: "; test(positiveAndNegativeFloats, 3); return 0; } |
Listing 1 |
Because
std::lowest()
is not available in C++98, an alternative might be needed. I created a second example which uses the traits technique (see Listing 2).
#include <cstdlib> #include <cmath> #include <limits> #include <iostream> #include <string> template< class T > T findMax(const T * data, const size_t numItems) { // Obtain the minimum value for type T T largest = std::numeric_limits< T >::min(); for (unsigned int i = 0; i<numItems; ++i) if (data[i] > largest) largest = data[i]; return largest; } namespace detail { template< class T, bool isFloat > struct cpp98_numeric_limits_lowest {}; template< class T > struct cpp98_numeric_limits_lowest<T, true> { static T value() { return -std::numeric_limits<T>::max(); } }; template< class T > struct cpp98_numeric_limits_lowest<T, false> { static T value() { return std::numeric_limits<T>::min(); } }; } // end namespace detail template< class T > T cpp98_numeric_limits_lowest() { return detail::cpp98_numeric_limits_lowest< T, std::numeric_limits<T>::is_specialized && !std::numeric_limits<T>::is_integer> ::value(); } template< class T > T findMaxNew(const T * data, const size_t numItems) { // Obtain the minimum value for type T T largest = cpp98_numeric_limits_lowest<T>(); for (unsigned int i = 0; i<numItems; ++i) if (data[i] > largest) largest = data[i]; return largest; } template< class T > void printAry(const T * data, const size_t numItems) { for (unsigned int i = 0; i<numItems; ++i) std::cout << data[i] << " "; std::cout << std::endl; } namespace detail { template< typename T, bool isFloat > struct areEqual {}; template< typename T > struct areEqual<T, true > { static bool value(const T& a, const T& b) { return std::abs(a - b) <= std::numeric_limits<T>::epsilon(); } }; template< class T > struct areEqual<T, false > { static bool value(const T& a, const T& b) { return a == b; } }; } // end namespace detail template< class T > T areEqual(const T& a, const T& b) { return detail::areEqual< T, std::numeric_limits<T>::is_specialized && !std::numeric_limits<T>::is_integer> ::value(a, b); } template< class T > void test(const std::string& desc, const T * data, const size_t numItems) { std::cout << "array of " << desc << ": "; printAry(data, numItems); T maxval = findMax<T>(data, numItems); T newMaxval = findMaxNew<T>(data, numItems); std::cout << "max in " << desc << " is " << maxval; if (!areEqual<T>(maxval, newMaxval)) std::cout << " but should have been " << newMaxval; std::cout << std::endl << std::endl; } int main() { {float vals[] = { -2, -1, -3 }; test<float>("only negative floats", vals, 3); } {float vals[] = { 2, -1, -3 }; test<float>("positive and negative floats", vals, 3); } {double vals[] = { -2, -1, -3 }; test<double>("only negative doubles", vals, 3); } {double vals[] = { 2, -1, -3 }; test<double>("positive and negative doubles", vals, 3); } {long double vals[] = { -2, -1, -3 }; test<long double>("only negative long doubles", vals, 3); } {long double vals[] = { 2, -1, -3 }; test<long double>("positive and negative long doubles", vals, 3); } {int vals[] = { -2, -1, -3 }; test<int>("only negative ints", vals, 3); } {int vals[] = { 2, -1, -3 }; test<int>("positive and negative ints", vals, 3); } {unsigned short vals[] = { 2, 3, 1 }; test<unsigned short>("unsigned shorts", vals, 3); } return 0; } |
Listing 2 |
Please note that it could be possible to distinguish at compile time between pre and post C++11 so that newer compilers could be routed to
lowest()
directly, but I couldn’t find a short and clean way to do that, although
#if (__cplusplus > 199711L)
would come pretty close for many compilers.
Regards,
Lion Gutjahr
References
[Frogley01] Frogley, Thaddaeus ‘An introduction to C++ Traits’, Overload Journal #43 (June 2001)
[N1880] N1880 proposal to the C++ standard http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1880.htm
[N2348] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2348.pdf
Response from Thaddaeus
If anything, it highlights the need to introduce lowest, if there is (was) no clear and straightforward way to achieve the same goal in the C++ of 2001, noting that
-max
doesn’t work for unsigned integers!
My preferred solution these days would to write something like this (and this is also a more language agnostic approach):
template< class T > T findMax(const T* data, int numItems) { assert(numItems>0); // Obtain the minimum value for type T int i=0; T largest = data[i++]; for (;i < numItems; ++i) if (data[i] > largest) largest = data[i]; return largest; }
which avoids the problem altogether, provided
numItems != 0
.