Author:             Fernando Cacciola
Contact:            fernando.cacciola@gmail.com
Organization:    SciSoft
Date:                 2005-08-29
Number:            N1879=05-0139

A proposal to add a general purpose ranged-checked numeric_cast<> (Revision 1)

Motivation and Scope

An out-of-range conversion occurs when a source value cannot be presented in a destination type, not even approximately between 2 adjacent destination values.

If the destination type is unsigned, the conversion, even if logically out-of-range, if always defined (4.7.2) as the least congruent unsigned value.

If the destination type is a signed integer, the result of an out-of-range conversion is implementation-defined (4.7.3).

If the destination type is floating-point, or the source type is floating-point and the destination type integer, an out-of-range conversion is undefined-behavior (4.8.1,4.9.1).

Clearly, out-of-range conversions are a problem because they can involve from implementation-defined to undefined behavior. Even signed to unsigned conversion can be a problem because the resulting value, even if well defined, can be as wrong as any arbitrary unrelated value.

Numerical applications, ranging from sophisticated scientific software to modeling to accounting to simple number manipulation can rarely disregard the problems associated with out-of-range conversions. In consequence, most such applications incorporate ad-hoc out-of-range checking code and protocols.

Furthermore, most C++ users are unaware of these rules. For instance, most C++ programmers are unaware that an out-of-range floating point conversion is undefined behavior (...and can for instance cause the client's hard disk to be wiped out entirely, as the say goes)

Generic Programming came into C++ along with the need for a numerical conversion utility providing well-defined behavior for all generic conversion cases (all combinations of source and destination types). So much in fact, that the Boost project launched, 6 years ago, with such an utility as part of its initial offering.

Whether boost::numeric_cast<> or any other in-house facility, most numerical applications have been using range-checked numeric conversions from a long time.

But there is a problem: properly determining whether a source value is in range for a given destination type is tricky because involves comparing a value of a type with the boundary values of a different type, task that itself requires a numeric conversion. This is in fact so difficult that boost::numeric_cast<> had it wrong for certain cases for years, even after a number of revisions.

A numeric_cast<> is therefore a perfect candidate for standardization because it is widely needed but its implementation much too difficult to ask users to roll their own (or need one from a non standard library)

The Boost Numeric Conversion Library introduced a re-implementation of boost::numeric_cast<> which using metaprogramming fixed all the range-checking problems of the initial version and even gained compile-time optimizations by totally avoiding the range-check when unnecessary.

That library, which serves as a reference implementation, developed the right concepts that are needed to formulate a proper range-checking logic for any combination of types (that is, a formulation that prevents the conversions needed by the check itself to be out-of-range). It also shown that template-metaprogramming can be used to optimize away the range-checking when unneeded.

The Boost library uses a policy-based design that offers users a wide latitude of choices (user-defined-types support, custom out-of-range handling, float to integer rounders, etc), but the facility proposed in this paper for standardization is a simplified version more in the spirit of the original boost numeric_cast<>

The proposed numeric_cast<T>(s) shall give the same result as static_cast<T>(s) (that is, the conversion itself is a just standard conversion) but with an added out-of-range check which implementations are required to bypass if unneeded (the proposed text defines exactly when is this the case). The out-of-range check is itself performed separately by another proposed function: bool is_out_of_range<T>(s) which users can call if they need a domain-specific out-of-range response. If numeric_cast<T>(s) detects an out of range condition, an out-of-range installable handler is called.

Impact On the Standard

The proposed text is intended to allow upcoming numeric types like decimal types, big integers, rationals, etc to be usable with the facility. To that effect, the proposed text includes some definitions which encompasses these eventually-standard types. Additionally, the proposed text intends to allow a C++ program to extend the standard numeric_cast<> for user-defined-types.

Nonetheless, even though the facility must interact with other pieces of the standard, the proposal takes the form of a pure extension. There are only additions.

Design Decisions

Features:

The fundamental need that this proposal aims to satisfy is providing range-checked standard conversions because range checking is a general need in a wide range of application domains. But there are other needs: for example, in converting a floating-point type to an integer type it might be critical to be able to control how the fractional part is handled (instead of simply discarding it as a standard conversion does), or maybe an application needs to trap inexact (but in-range) conversions (when the source value is represented as one of the two adjacent values in the destination type); or "fix" out of range conversions by choosing the closest boundary value in the destination type. Yet from all this features, range checking is the only one that doesn't need to redefine the conversion itself because it can be provided as a precondition to a standard conversion.

It is certainly possible to design a facility to give users further control over the conversion process. To some extent, C99 and the Boost Numeric Conversion Library do that. However, the pieces that would be directly involved and affected are yet to be standardized (additions from C99, additional numeric types, like decimal, integer, rational, etc..) so is more convenient to take it one step at a time and standardize at this point only the simplest solution to the more general problem: range checking.

Interface:

A policy-based design like that used in the Boost Numeric Conversion library is certainly a reasonable way to give users the extra latitude, but such designs has as a drawback that the resulting behavior is on the hands of the users. For such a fundamental part of a language as its standard library, just carefully specifying the requirements and expected behavior for the policies is simply not enough. There is no discussion about the potential and usefulness of policies, but the actual instrumentation of such designs needs tools from the language, like concepts and design-by-contract forms, which are not yet available (but these have been already formally proposed). Thus, the policy-based design of the Boost version has been dropped in favor of a simpler but better defined facility.

Out of range response:

The canonical way to treat exceptional errors in C++ is by throwing an exception. However, numeric conversions are specially important in numeric applications and numeric applications are particularly common in restricted platforms like embedded systems. In consequence, always throwing an exception in the presence of an out-of-range conversion is unlikely to be welcome by the numerical community which is intended to be a primary target. Thus, the proposed solution uses an installable handler (like the new handler) so that implementations and programs can customize the out-of-range response to suit the target platform.

Types supported:

At this point in the C++ standard life-cycle, a number of numeric types are being proposed. This proposal assumes that at least some of these types will be accepted so it tries to cover the conversions involving not only arithmetic types but these eventual standard numeric types as well. Since the proposed numeric_cast<>() only adds range checking but doesn't define the conversion itself, the proposal only has to properly cover the range checking logic for any type it intends to specify behavior. To achieve that, the specification of the range checking logic that implementations must follow is formulated with the assumption that the implementation has complete access to the details of these standard numeric types. In particular, the implementation is required to be able to test whether a boundary value of any given standard numeric type is representable in any other given standard numeric type.

Furthermore, the proposal adds a simple provision to allow C++ programs to make their user-defined-types be usable with numeric_cast<>: they are allowed to provide additional specializations, even partial (assuming the language eventually allows that), of the template function.

Proposed Text for the Standard

A.    Add the following to 17.4.3.5 [lib.handler.functions]

  1. out_of_range_handler
  2. set_of_out_range_handler

B. Add the following to the synopsis for <numeric> at 26.4, [lib.numeric.ops]

template<class T, class S> bool is_out_of_range ( S s ) throw() ;
template<class T, class S> T numeric_cast ( S s ) throw ( range_error ) ;

C. Add the following subclauses:

17.1.21 Numeric type                                                             [defs.numeric.type]

any type which satisfies the numeric type requirements (26.1)

 [Note: a user defined type can fit this definition -end note]

17.1.22 Standard numeric type                                                             [defs.numeric.type.std]

any numeric type (17.1.21) defined in this standard, including library provided types such as complex<>

17.1.23 Numeric conversion                                                              [defs.numeric.conversion]

any conversion, standard or user defined, involving numeric types (17.1.21)

17.1.24 Standard numeric conversion                                                              [defs.numeric.conversion.std]

any numeric conversion (17.1.23) involving standard numeric types (17.1.22)

17.1.25 Super-ranged conversions                                                           [defs.numeric.conversion.superranged]

standard numeric conversions from any value of a standard numeric type (17.1.2) S to a value of a  standard numeric type T such that either numeric_limits<T>::is_bounded==false, or both numeric_limits<S>::lowest()1 and numeric_limits<S>::max() can be represented in the type T, either exactly or between two adjacent T values. That is, conversions for which any value of the source type can be represented in the destination type.

17.1.26 Sub-ranged conversion                                                             [defs.subranged]

standard numeric conversions from any value of a standard numeric type (17.1.2) S to a value of a  standard numeric type T such that mumeric_limits<T>::is_bounded==true and  numeric_limits<S>::lowest()1 or numeric_limits<S>::max() cannot be represented in the type T, neither exactly nor between two adjacent T values. That is, conversions for which some values of the source type cannot be represented in the destination type.
If conversions from S to T are sub-ranged, then necessarily, conversions in the opposite direction, T->S, are super-ranged. The converse is not necessarily true (for example, a conversion from N->N is super-ranged in both directions)

18.7.1 Type out_of_range_handler                                                                     [lib.support.runtime.outofrange]

typedef void (*out_of_range_handler)();

1.    The type of the handler function called by numeric_cast<> (26.4.5) when an out of range is detected

2.    Required behavior: an out_of_range_handler shall perform one of the following::

        - throw an exception of type range_error (19.1.7) or a class derived from range_error

        - call either abort() or exit()

        - return

3.    An out of range handler installed by a program or the implementation is allowed to perform additional operations, with side effects, prior to the required operation, provided they do not incur undefined behavior.

    [Note: allowing additional operations is intended to let a C++ program properly register and report the incident; not to attempt to recover from the error -endnote]

18.7.2 set_out_of_range_handler                                                                     [lib.support.runtime.set.outofrange]

out_of_range_handler set_out_of_range_handler( out_of_range_handler new_h ) throw() ;

1.       Effects: Establishes the function designated by new_h as the current out_of_range_handler.

2.       Returns: 0 on first call, the previous out_of_range_handler on subsequent calls.

26.4.5 Out of range check                                                                    [lib.numeric.outofrangecheck]

template<class T, class S> bool is_out_of_range ( Source s ) throw() ;

1.    Returns:

        A. If conversions from S to T are super-ranged (17.1.22), false for any value of s [Note: this case can only be applied if both T and S are standard numeric types (17.1.22), per definition of super-ranged  -end note]

        B. If conversions from S to T are sub-ranged  (17.1.23); the result of evaluating this expression:

           (s >= static_cast<S>(numeric_limits<T>::lowest())1
        && (s <= static_cast<S>(numeric_limits<T>::max   ()).

             [Note: this case can only be applied if both S and T are standard numeric types (17.1.22), per definition of sub-ranged  -end note]

             [Note: when S->T are sub-ranged; T->S are super-ranged so the expression can be evaluated without incurring undefined behavior]

        C.If either S or t are not standard numeric types (17.1.22), an unspecified result.

2.    A C++ program is allowed to defined specializations of this template function provided that S or T or both are user-defined-types. If both S and T are standard numeric type (17.1.22) the specializations ill-formed and the implementation is required to issue diagnostic.

26.4.6 Numeric_cast                                                                     [lib.numeric_cast]

template<class T, class S> T numeric_cast( S s ) throw ( range_error );

1.     The result of the expression numeric_cast<T>(s) is the same as the result of the expression static_cast<T>(s) with the following added restrictions and side-effects:

2.    If either S or T are not standard numeric types (17.1.22), the behavior is unspecified if there exist in the program a user-provided specialization of the function for  these types and undefined if such a user-provided specialization does not exist.

4.    If conversions from S to T are sub-ranged (17.1.23), is_out_of_range<T>(s) shall be called and if the result of that is true, the installed out_of_range-handler shall be invoked, if any.

5.    If conversions from S to T are super-ranged (17.1.22), is_out_of_range<T>(s) shall not be called and numeric_cast<T>(s) shall be distinguishable from static_cast<T>(s) only in the additional type requirements. That is, implementations are required to skip the overhead of range checking for super-ranged conversions.

6.    A C++ program is allowed to defined specializations of this template function provided that S or T or both are user-defined-types. If both S and T are standard numeric type (17.1.22) the specialization is ill-formed and the implementation is required to issue a diagnostic.

7.    The implementation is allowed to implement numeric_cast<> as an internal operator and not as a explicit library function provided it can diagnose a violation to subclause 6 in case a C++ programs adds specializations.

Footnotes:

  1. lowest() is the addition to numeric_limits<> proposed in N1881
  2. (which accompanies this proposal)

Acknowledgements

All the people in the boost community, particularly those involved in the development of the Boost.NumericConversion library.

Thorsten Ottosen for his help preparing this proposal.

References

  1. Boost.Numeric Conversion Library, http://www.boost.org/libs/numeric/conversion/doc/index.html, Fernando Cacciola
  2. Standard Documents:
    1. ISO/IEC 9899:1999 (C99 Standard)
    2. ISO/IEC 10967-1 (Language Independent Arithmetic (LIA), Part I, 1994)
    3. ISO/IEC 2382-1:1993 (Information Technology - Vocabulary - Part I: Fundamental Terms)
    4. ANSI/IEEE 754-1985 [and IEC 60559:1989] (Binary floating-point)
    5. ANSI/IEEE 854-1988 (Radix Independent floating-point)
    6. ANSI X3/TR-1-82 (Dictionary for Information Processing Systems)
    7. ISO/IEC JTC1/SC22/WG14/N753 C9X Revision Proposal: LIA-1 Binding: Rationale
  3. Papers:
    1. David Goldberg What Every Computer Scientist Should Know About Floating-Point Arithmetic
    2. Prof. William Kahan papers on floating-point.