Add type-safe minimum and maximum type-generic macros

Jens Gustedt, INRIA and ICube, France

2025-04-29

target

integration into IS ISO/IEC 9899:202y

document history

document number date comment
n3543 202504 this document

1 Introduction

Generic features to compute minimum and maximum value of two given values is notoriously challenging in C.

2 Approach

We propose to add type-generic macros that solve all these problems. Namely:

In particular the choice for the minimum differs from the usual arithmetic conversions. If one type is signed and the other is unsigned the macro ckd_min chooses the signed type for the result.

Possible problems arise

For these cases we propose different variants, one that rejects translation in these cases, one that makes these cases implementation-defined, including the possibility to reject translation, and one that has undefined behavior.

For convenience, we propose to add the features to the <stdckdint.h> header with the idea this header contains operations that are merely language features that always have defined behavior. The suggested wording could easily be adapted to be placed in a different header (clause 7) or even to be introduced as proper operators (clause 6).

3 Implementations

Implementations of minimum and maximum functions there are plenty. Even the current C standard provides a handful of slightly different interfaces, even for type-generic ones. Then, in the field an abundant number of implementations of varying properties and quality are added to that picture.

We are not aware of an implementation of these functionalities that all the problems that are listed in the introduction. In particular, none of them seems to provide an integer constant expression where this would be possible. This is all the more surprising as that property has a clear demand (in particular for array lengths) and as providing macros that have that property is not too difficult by using either implementation-specific extensions (such as gcc’s __builtin_constant_p) and/or playing tricks with _Generic.

The goal of this paper is to propose a unification to all these interfaces and their behavior, such that programmers find reliable implementations of these features in their C library. The reference implementation that we maintain (and are able to provide on request) is not meant to suggest any particular way in which these features should be implemented, but only to prove that an implementation is possible as of today with minimal effort.

4 Suggested Wording

In 7.20.1, bump the value of __STDC_VERSION_STDCKDINT_H__.

Then add a new clause

7.20.4 Minimum and maximum operation type-generic macros

Synopsis

#include <stdckdint.h>
minType ckd_min(typeA a, typeB b);
maxType ckd_max(typeA a, typeB b);

Description

2 These type-generic macros compute the minimum and maximum value of their arguments, respectively. If both arguments are integer constant expressions, the macro call expression is also an integer constant expressions.
3 The result type maxType is typeof(a+b). The result type minType is determined as follows:

Returns

4 If none of the arguments is a NaN, the result of the operation is the mathematically lesser (respectively greater) argument converted to type minType and maxType, respectively. Otherwise, if one of the arguments is a signaling NaN, the “domain error” floating point exception is raised and the result is a signaling NaN of the appropriate type. Otherwise, if one of the arguments is a quiet NaN, no floating point exception is raised and the result is a quiet NaN of the appropriate type.

5 NOTE 1 If both arguments have integer type, minType and maxType are able to represent the mathematical result of the respective operation.

6 NOTE 2 For other type combinations (e.g combining a uint64_t integer and an ISO/IEC 60559 binary64 floating point type) the types minType and maxType (here the floating point type) is not able to hold the precise result of the operations for all possible combinations of argument values.
7 Example
#include <ckdint.h>
constexpr size_t n = SIZE_MAX;
static double A[ckd_max(n, 1)];          // valid
constexpr auto ms = ckd_min(n, 0);       // valid, type is int
constexpr auto mu = ckd_min(n, 0u);      // valid, type is size_t
constexpr auto mc = ckd_min(n, (char)0); // valid, type depends on architecture
constexpr auto md = ckd_min(n, 0.0);     // type double, possibly invalid
Here, the array length expression of A computes the maximum of two integer constant expressions and is thus an integer constant expression, too. For ms, one of the arguments to ckd_min has a signed type, so the result type is that signed type. Conversely, for mu both arguments have an unsigned type, and so the result is the common real type. The type of mc depends on whether or not char admits negative values; if so, the result type is int, otherwise it is size_t. For md the common real type is double, but it is implementation-defined if the value of SIZE_MAX is representable without loss of precision in that type.

5 Variants

It is important to add the possibility to reject combinations that cannot properly represent the mathematical result in all cases. Therefore one of the following variants should be added to the text above.

5.1 Variant “translation failure”

Add to 7.20.4 as described above

3′ If typeA and typeB do not have a common real type or if the result type of the operation is not able to hold the result for all possible value combinations of typeA and typeB, the program translation fails.

Add to the end of 7.20.4 p7 (example) as described above

If it is, all possible value combinations of values are representable in the result and the definition of md is accepted; otherwise the program translation fails.

Add to J .3.16, architecture specific behavior

5.2 Variant “implementation-defined”

Add to 7.20.4 as described above

3′ If typeA and typeB do not have a common real type it is implementation-defined if the program translation fails or if the types minType and maxType are chosen in an implementation-defined way. If the result type of the operation is not able to hold the result for all possible value combinations of typeA and typeB, it is implementation-defined if the operation is accepted, possibly resulting in a loss of precision, or if the program translation fails.

Add to the end of 7.20.4 p7 (example) as described above

If it is, the definition of md is accepted. Otherwise it is implementation-defined if the program translation fails or if md has the value (double)SIZE_MAX.

Add to J .3.15, implementation-defined behavior

5.3 Variant “undefined behavior”

Add to 7.20.4 as described above

3′ If typeA and typeB do not have a common real type or if the result type of the operation is not able to hold the result for all possible value combinations of typeA and typeB, the behavior is undefined.

Add to the end of 7.20.4 p7 (example) as described above

If it is, the definition of md is valid, otherwise the behavior is undefined.

Add to J .2, undefined behavior