C++: Why does numeric_limits work on types it does not know?
The class template std::numeric_limits
was added as a replacement for the macros from <limits.h>
before template meta programming was a thing: it was in the pre-standard publicly circulated drafts (~1995). Template meta programming was invented by Erwin Unruh around the Stockholm meeting (July 1996). At this point nobody thought whether it could be detectable that a class template is defined. Instead, std::numeric_limits<T>::is_specialized
would indicate (at compile-time) whether the class template is specialized and meaningful for type T
. The various member were defined to use a reasonable default which would make it likely to get the code compiled although generic would be implemented such that it doesn't use any of the values for types which are not specialized.
With std::numeric_limits
being specified like that in the C++ standard it wouldn't change without a very good reason: any change would likely break somebody's code - even if this code could be done better with now discovered technology (some of which was genuinely unavailable with C++98). The committee wouldn't design the traits like this now: the type traits in <type_traits>
are stand-alone traits - although generally still defined for all viable types with a suitable default. However, there is also no reason to change std::numeric_limits
in a breaking way as the current definition does work.
Note that not all members of std::numeric_limits<T>
are valid for all types T
. For example use of std::numeric_limits<T>::max()
will fail to compile if T
's default constructor is unaccessible, unavailable, or delete
d. So, you'd be best off guarding access to any of the members on whether the class template is specialized (using C++17):
template <typename T>
void f() {
if constexpr (std::numeric_limits<T>::is_specialized) {
// use of std::numeric_limits<T>::max(), min(), etc.
}
else {
// implement the rquired functionality differently
}
}
Is it permissible to specialize mathematical constants for custom numeric types?
According to C++20 draft it seems fine:
26.9.2
(...)
2. Pursuant to 16.5.4.2.1, a program may partially or explicitly specialize a mathematical constant variable
template provided that the specialization depends on a program-defined type
Determining if ::std::numeric_limitsT is safe to instantiate
C++11 solutions
Since T
only appears as the return type of static member functions in the declarations of the unspecialised ::std::numeric_limits<T>
(see C++03 18.2.1.1 and C++11 18.3.2.3), it is enough for this specific problem to ensure that doing so is declaration-safe.
The reason this leads to a compile time error is, that the use of a template-argument may not give rise to an ill-formed construct in the instantiation of the template specialization (C++03 14.3/6, C++11 14.3/6).
For C++11 enabled projects, Andy Prowl's can_be_returned_from_function
solution works in all relevant cases: http://ideone.com/SZB2bj , but it is not easily portable to a C++03 environment. It causes an error in when instantiated with an incomplete type ( http://ideone.com/k4Y25z ). The proposed solution will accept incomplete classes instead of causing an error. The current Microsoft compiler (msvc 1700 / VS2012) seems to dislike this solution and fail to compile.
Jonathan Wakely proposed a solution that works by utilizing std::is_convertible<T, T>
to determine if T
can be the return value of a function. This also eliminates incomplete classes, and is easy to show correct (it is defined in C++11 to do exactly what we want). Execution shows that all cases (arrays, arrays of undefined length, functions, abstract classes) which are known to be problematic are correctly recognized. As a bonus, it also correctly recognizes incomplete classes, which are not allowed as parameters to numeric_limits
by the standards (see below), although they seem to cause no problems in practice, as long as no problematic functions are actually called. Test execution: http://ideone.com/zolXpp . Some current compilers (icc 1310 and msvc 1700, which is VS2012's compiler) generate incorrect results with this method.
Tom Knapen's is_arithmetic
solution is a very concise C++11 solution, but requires the implementer of a type that specialises numeric_limits
to also specialise is_arithmetic
. Alternatively, a type that in its base case inherits from is_arithmetic
(this type might be called numeric_limits_is_specialised
) might be specialised in those cases, since specialising is_abstract
might not be semantically correct (e.g. a type that does not specify all basic arithmetic operators, but still is a valid integer-like type).
This whitelisting approach ensures that even incomplete types are handled correctly, unless someone maliciously tries to force compilation errors.
Caveat
As shown by the mixed results, C++11 support remains spotty, even with current compilers, so your mileage with these solutions may vary. A C++03 solution will benefit from more consistent results and the ability to be used in projects that do not wish to switch to C++11.
Towards a robust C++03 solution
Paragraph C++11 8.3.5/8 lists the restrictions for return values:
If the type of a parameter includes a type of the form "pointer to array of unknown bound of T" or "reference to array of unknown bound of T", the program is ill-formed. Functions shall not have a return type of type array or function, although they may have a return type of type pointer or reference to such things. There shall be no arrays of functions, although there can be arrays of pointers to functions.
and goes on in paragraph C++11 8.3.5/9:
Types shall not be defined in return or parameter types. The type of a parameter or the return type for a function definition shall not be an incomplete class type (possibly cv-qualified) unless the function definition is nested within the member-specification for that class (including definitions in nested classes defined within the class).
Which is pretty much the same as paragraph C++03 8.3.5/6:
If the type of a parameter includes a type of the form "pointer to array of unknown bound of T" or "reference to array of unknown bound of T", the program is ill-formed. Functions shall not have a return type of type array or function, although they may have a return type of type pointer or reference to such things. There shall be no arrays of functions, although there can be arrays of pointers to functions. Types shall not
be defined in return or parameter types. The type of a parameter or the return type for a function definition shall not be an incomplete class type (possibly cv-qualified) unless the function definition is nested within the member-specification for that class (including definitions in nested classes defined within the class).
Another kind of problematic types is mentioned identically in C++11 10.4/3 and C++03 10.4/3:
An abstract class shall not be used as a parameter type, as a function return type, or as the type of an explicit conversion. [...]
The problematic functions are not nested within an incomplete class type (except of ::std::numeric_limits<T>
, which cannot be their T
), so we have four kinds of problematic values of T
: Arrays, functions, incomplete class types and abstract class types.
Array Types
template<typename T> struct is_array
{ static const bool value = false; };
template<typename T> struct is_array<T[]>
{ static const bool value = true; };
template<typename T, size_t n> struct is_array<T[n]>
{ static const bool value = true; };
detects the simple case of T
being an array type.
Incomplete Class Types
Incomplete class types interestingly do not lead to a compilation error just from instantiation, which means either the tested implementations are more forgiving than the standard, or I am missing something.
C++03 example: http://ideone.com/qZUa1N
C++11 example: http://ideone.com/MkA0Gr
Since I cannot come up with a proper way to detect incomplete types, and even the standard specifies (C++03 17.4.3.6/2 item 5)
In particular, the effects are undefined in the following cases: [...] if an incomplete type (3.9) is used as a template argument when instantiating a template component.
Adding only the following special allowance in C++11 (17.6.4.8/2):
[...] unless specifically allowed for that component
it seems safe to assume that anybody passing incomplete types as template parameters are on their own.
A complete list of the cases where C++11 allows incomplete type parameters is quite short:
declval
unique_ptr
default_delete
(C++11 20.7.1.1.1/1: "The class template default_delete serves as the default deleter (destruction policy) for the class templateunique_ptr
."shared_ptr
weak_ptr
enable_shared_from_this
Abstract Class & Function Types
Detecting functions is a bit more work than in C++11, since we do not have variadic templates in C++03. However, the above quotes on functions already contain the hint we need; functions may not be elements of arrays.
Paragraph C++11 8.3.4\1 contains the sentence
T is called the array element type; this type shall not be a reference type, the (possibly cv qualified) type void, a function type or an abstract class type.
which is also verbatim in paragraph C++03 8.3.4\1 and will allow us to test if a type is a function type. Detecting (cv) void
and reference types is simple:
template<typename T> struct is_reference
{ static const bool value = false; };
template<typename T> struct is_reference<T&>
{ static const bool value = true; };
template<typename T> struct is_void
{ static const bool value = false; };
template<> struct is_void<void>
{ static const bool value = true; };
template<> struct is_void<void const>
{ static const bool value = true; };
template<> struct is_void<void volatile>
{ static const bool value = true; };
template<> struct is_void<void const volatile>
{ static const bool value = true; };
Using this, it is simple to write a meta function for abstract class types and functions:
template<typename T>
class is_abstract_class_or_function
{
typedef char (&Two)[2];
template<typename U> static char test(U(*)[1]);
template<typename U> static Two test(...);
public:
static const bool value =
!is_reference<T>::value &&
!is_void<T>::value &&
(sizeof(test<T>(0)) == sizeof(Two));
};
Note that the following meta function may be used to distinguish between the two, should one wish to make a distinct is_function
and is_abstract_class
template<typename T>
class is_class
{
typedef char (&Two)[2];
template<typename U> static char test(int (U::*));
template<typename U> static Two test(...);
public:
static const bool value = (sizeof(test<T>(0)) == sizeof(char));
};
Solution
Combining all of the previous work, we can construct the is_returnable
meta function:
template<typename T> struct is_returnable
{ static const bool value = !is_array<T>::value && !is_abstract_class_or_function<T>::value; };
Execution for C++03 (gcc 4.3.2): http://ideone.com/thuqXY
Execution for C++03 (gcc 4.7.2): http://ideone.com/OR4Swf
Execution for C++11 (gcc 4.7.2): http://ideone.com/zIu7GJ
As expected, all test cases except for the incomplete class yield the correct answer.
In addition to the above test runs, this version is tested (with the exact same test program) to yield the same results w/o warnings or errors on:
- MSVC 1700 (VS2012 with and w/o XP profile), 1600 (VS2010), 1500 (VS2008)
- ICC Win 1310
- GCC (C++03 and C++11/C++0x mode) 4.4.7, 4.6.4, 4.8.0 and a 4.9 snapshot
Restrictions for either case
Note that, while this approach in either version works for any numeric_limits
implementation that does not extend upon the implementation shown in the standard, it is by no means a solution to the general problem, and in fact may theoretically lead to problems with weird but standard compliant implementations (e.g. ones which add private members).
Incomplete classes remain a problem, but it seems silly to require higher robustness goals than the standard library itself.
Correct way to restrict functions to specific data types
You can take a look in boost/multiprecision/mpfr.hpp
how mpfr_float_...
is defined. These types are themselves templates
typedef number<mpfr_float_backend<50> > mpfr_float_50;
typedef number<mpfr_float_backend<100> > mpfr_float_100;
typedef number<mpfr_float_backend<500> > mpfr_float_500;
typedef number<mpfr_float_backend<1000> > mpfr_float_1000;
typedef number<mpfr_float_backend<0> > mpfr_float;
Based on this observation we can easily come up with a template which only matches those types:
#include <iostream>
#include <boost/multiprecision/mpfr.hpp>
using boost::multiprecision::number;
using boost::multiprecision::backends::mpfr_float_backend;
template < unsigned size >
void print(number<mpfr_float_backend<size>> const &num)
{
std::cout << "mpfr_float_" << size << ": " << num << "\n";
}
int main()
{
using namespace boost::multiprecision;
mpfr_float_50 a = 1;
mpfr_float_100 b = 2;
mpfr_float_500 c = 3;
double d = 4;
print(a);
print(b);
print(c);
//print(d); // BOOM!
}
You could of course also be a little more general and allow any Boost.Multiprecision type:
template < typename... T >
void print(boost::multiprecision::number<T...> const &num)
{
std::cout << num << "\n";
}
Related Topics
Ways to Detect Whether a C++ Virtual Function Has Been Redefined in a Derived Class
Adding Signals/Slots (Qobject) to Qgraphicsitem: Performance Hit
What Is the Purpose of the _Chkstk() Function
Does Copy List Initialization Invoke Copy Ctor Conceptually
Static Variable in the Class Declaration or Definition
Deleted Function in Std::Pair When Using a Unique_Ptr Inside a Map
Undefined Reference to a Static Member of the Class
C++ - Std::Thread Crashes Upon Execution
Differencebetween Include_Directories and Target_Include_Directories in Cmake
Narrowing Conversion from Unsigned to Double
How to Make an Image Resize to Scale in Qt
C++ Templates: Conditionally Enabled Member Function
Setting Pointer to Arbitrary Dimension Array
Undefined Symbol on a Template Operator Overloading Function