handle NB comments concerning constexpr

Alex Gilding (Perforce UK)

Jens Gustedt (INRIA France)

2023-01-08

org: ISO/IEC JCT1/SC22/WG14 document: N3078
target: IS 9899:2023 version: 1
date: 2023-01-08 license: CC BY

GB-014, 083, 146, 149, 152, and 163

These comments all look legitimate to us. We revise them and show them in their respective context and then propose to poll them together in one go.

GB-014

Stating that floating values shall be constant expressions ensures they can be used in static initializers, but the stronger condition of being arithmetic constant expressions (rather than some other kind of implementation-defined constant expression permitted in initializers) is needed for them to be usable in constexpr initializers.

Assuming those macros should be usable in constexpr initializers, change “all floating values shall be constant expressions” to “all floating values shall be arithmetic constant expressions”.

In the context of 5.2.4.2.2 p15

15 All integer values in the <float.h> header, except FLT_ROUNDS, shall be constant expressions suitable for use in conditional expression inclusion preprocessing directives; all floating values shall be arithmetic constant expressions. All except CR_DECIMAL_DIG (F.5), DECIMAL_DIG, DEC_EVAL_METHOD, FLT_EVAL_METHOD, FLT_RADIX, and FLT_ROUNDS have separate names for all floating types. The floating-point model representation is provided for all values except DEC_EVAL_METHOD, FLT_EVAL_METHOD and FLT_ROUNDS.

GB-083

When should it be possible to initialize an object of structure or union type with a named constant or compound literal constant of such type? The current wording seems to indicate that it is only possible at automatic storage duration – but when such initialization occurs, it is permitted even when the object being initialized is constexpr. Since the normal rule is that the requirements on constexpr initializers are stricter than those on static storage duration initializers, it is an anomaly to allow named and compound literal constant of structure or union type in constexpr initializers of automatic storage duration but not in any initializers of static storage duration.

Assuming such initializers are intended to be allowed at static storage duration, in paragraph 14, remove “that has automatic storage duration”. Add a footnote to the end of the paragraph: “If the object being initialized does not have automatic storage duration, this case violates a constraint unless the expression is a named constant or compound literal constant (6.6).”. Also remove “that has automatic storage duration” in J.2 item 81.

We agree that this has been an oversight. The proposed changes in context look as follows. For 6.7.10 p14

14 The initializer for a structure or union object that has automatic storage duration shall be either an initializer list as described below, or a single expression that has compatible structure or union type. In the latter case, the initial value of the object, including unnamed members, is that of the expression.FNT)

FNT) If the object being initialized does not have automatic storage duration, this case violates a constraint unless the expression is a named constant or compound literal constant (6.6).

For J.2

( 81) The initializer for a structure or union object that has automatic storage duration is neither an initializer list nor a single expression that has compatible structure or union type (6.7.10).

GB-146

The introduction of the keyword constexpr provides a new way to declare an identifier with internal linkage at file scope. Since such a new feature should not be immediately considered obsolescent, presumably it should only be the pre-existing case (of redeclaring without static after an initial declaration with static) that is listed as obsolescent.

Change “static” to “static or constexpr”.

In the context of 6.11.2 this looks as follows.

Declaring an identifier with internal linkage at file scope without the static or constexpr storage-class specifier is an obsolescent feature.

GB-149

To ensure usability in constexpr initializers, the _Complex_I and _Imaginary_I macros should be required to expand to arithmetic constant expressions.

Change “constant expression” to “arithmetic constant expression” in paragraphs 5 and 6.

In the context of 7.3.1 p5 and p6 that reads

5 The macro

complex

expands to _Complex; the macro

_Complex_I

expands to an arithmetic constant expression of type float _Complex, with the value of the imaginary unit.242)

6 The macros

imaginary

and

_Imaginary_I

are defined if and only if the implementation supports imaginary types243); and, if defined, they expand to _Imaginary and an arithmetic constant expression of type float _Imaginary with the value of the imaginary unit.

GB-152

The CMPLX macros should expand to arithmetic constant expressions to be usable in constexpr initializers, not just those of static or thread storage duration. (But if the implementation has floating expressions that are valid in static initializers but not in constexpr initializers, CMPLX calls with such arguments should have the same property.)

At the end of the paragraph, add another sentence: “The resulting expression shall be an arithmetic constant expression, provided both arguments are arithmetic constant expressions.”.

Put into the context of 7.3.9.3:

2 The CMPLX macros expand to an expression of the specified complex type, with the real part having the (converted) value of x and the imaginary part having the (converted) value of y. The resulting expression shall be suitable for use as an initializer for an object with static or thread storage duration, provided both arguments are likewise suitable. The resulting expression shall be an arithmetic constant expression, provided both arguments are arithmetic constant expressions.

GB-163

To be usable in constexpr initializers, constant expressions should be specified as arithmetic constant expressions.

Change “constant expression” to “arithmetic constant expression”, in paragraphs 5, 6, 7, 8, 9 and 10.

In the context of 7.12

5 The macro

HUGE_VAL

expands to a double arithmetic constant expression, not necessarily representable as a float, whose value is the maximum value returned by library functions when a floating result of type double overflows under the default rounding mode, either maximum finite number in the type or positive or unsigned infinity. The macros

HUGE_VALF
HUGE_VALL

are respectively float and long double analogs of HUGE_VAL278) .

6 The macros in this paragraph are only present if the implementation defines __STDC_IEC_60559_DFP__ and additionally the user code defines __STDC_WANT_IEC_60559_EXT__ before any inclusion of <math.h>. The macro

HUGE_VAL_D32

expands to an arithmetic constant expression of type _Decimal32 representing positive infinity. The macros

HUGE_VAL_D64
HUGE_VAL_D128

are respectively _Decimal64 and _Decimal128 analogs of HUGE_VAL_D32.

7 The macro

INFINITY

is defined if and only if the implementation supports an infinity for the type float. It expands to an arithmetic constant expression of type float representing positive or unsigned infinity.

8 The macro

DEC_INFINITY

expands to an arithmetic constant expression of type _Decimal32 representing positive infinity.

9 The macro

NAN

is defined if and only if the implementation supports quiet NaNs for the float type. It expands to an arithmetic constant expression of type float representing a quiet NaN.

10 The macro

DEC_NAN

expands to an arithmetic constant expression of type _Decimal32 representing a quiet NaN.

Poll

  1. Does WG14 want to apply the changes proposed in NB comments GB-014, 083, 146, 149, 152, and 163?

GB-080

Suppose a structure or union object is, as the whole or part of a constexpr initializer, initialized with a single expression (a named or compound literal constant) of that type (see comment below on 6.7.10 regarding the details of when exactly this is permitted). How exactly should the constraints that constexpr pointer initializers be null pointer constant be applied in this case, if the structure or union contains such a member (and, in the case of the union, the pointer is the active member or part of the active member)? Should it be a constraint violation because a hypothetical expression accessing the pointer member of the constant structure or union would not itself be a null pointer constant? Or should it be permitted because no such hypothetical expression is actually formed in the initialization process and the initializer actually written for the structure or union is a structure or union constant?

Supposing such an initialization should be permitted, replace “the implicit or explicit initializer value” by “any explicit initializer value”. Add a new Example:

struct s { void *p; };
constexpr struct s A = { nullptr };
constexpr struct s B = A;
/* Although the expression A.p is not a null pointer constant, only a null pointer, the only explicit initializer in the initialization of B is A, not A.p, so no constraint is violated by that initialization. */

If (see comment below on 6.7.10) such initializations are only accepted with automatic storage duration (if the proposed change for that comment is rejected), this example will need adjusting accordingly.

Proposed resolution

We propose a slightly simpler solution of this problem, namely to relax the constraints on the initialization of pointer constexpr objects.

6.7.1 Storage-class specifiers

Constraints

5 … If an object or subobject declared with storage-class specifier constexpr has pointer, integer, or arithmetic type, the implicit or explicit initializer value for it shall be a null pointer constant140), an integer constant expression, or an arithmetic constant expression, respectively.

140) The named constant or compound literal constant corresponding to an object declared with storage-class specifier constexpr and pointer type is a constant expression with a value null, and thus a null pointer and an address constant. Thus, such a named constant is a valid initializer for other constexpr declarations, provided the pointer types match accordingly. However, even if it has type void* it is not a null pointer constant.

15’ EXAMPLE 1’ Although in the following the expression A.p is not a null pointer constant, only a constant expression with pointer type and value null, the member-wise initialization of B with A is valid.

struct s { void *p; };
constexpr struct s A = { nullptr };
constexpr struct s B = A;

Poll

  1. Does WG14 want to apply the proposed solution for NB comment GB-80?

GB-081

The requirement on constexpr initializers that “The value of any constant expressions or of any character in a string literal of the initializer shall be exactly representable in the corresponding target type; no change of value shall be applied” is unclear in many cases, especially concerning floating point, regarding what exactly counts as a change of value when the type changes; the footnote and examples are only of limited value for clarifying this, and to the extent that the do clarify it, it is not clear that the results follow from the normative text. See attached document c2x-constexpr-init.pdf for more details of this issue.

The referred document is N3071

Resolution

The only values for which there seems to be ambiguity here are different categories of floating point values. This ambiguity stems from an unclear specification of what the term “value” comprises for floating point. This is not inherent to the constexpr proposal, it only puts the finger on it.

Therefore We refer to the FP SG to resolve this issue.

GB-125

May a declaration in a for loop use the constexpr storage class specifier (such an object being considered implicitly auto if no other storage class specifier used), or does “having storage class auto or register” exclude that case? (Cf. DR#277 regarding interpretation of that sentence.)

Change “having storage class auto or register” to “with automatic storage duration”, to avoid ambiguity (assuming the constexpr case should be allowed).

Solution proposed in the comment.

In principle, we agree with the change, but see below for other options. In the context of 6.8.5 this is

The declaration part of a for statement shall only declare identifiers for objects having storage class auto or registerwith automatic storage duration.

Static compound literals

But this puts to light another oversight, here. Now it would be possible to evaluate compound literals with a different storage duration in the context of such a declaration. We are not even sure about the reasons why other storage durations were not allowed here and if these reasons still apply today.

If we want objects obtained from compound literals to be the handled the same, this would need a change in 6.8.5

3 The declaration part of a for statement shall only declare identifiers for objects having storage class auto or register. The declaration and expression parts of an iteration statement shall only declare identifiers for objects or evaluate compound literals with automatic storage duration.

but also one in 6.8.4 where we would add a new constraint section

Constraints
1’ The expression part of a selection statement shall only evaluate compound literals with automatic storage duration.

Remove the constraint completely

As said, we are not sure why this constraint exists in the first place. If there is no such reason (anymore) we could also remove it completely.

In the context of 6.8.5 this is

The declaration part of a for statement shall only declare identifiers for objects having storage class auto or register.

Polls

  1. Does WG14 agree with the solution for NB comment GB-125 to remove the constraint completely?

  2. If not, does WG14 agree with the combined solution for NB comment GB-125 and static compound literals?

  3. If not, does WG14 agree with the proposed solution for NB comment GB-125?

GB-191

It might be appropriate to allow constexpr objects with static storage duration to be accessed in signal handlers.

Consider the following change: after “not a lock-free atomic object” insert “and that is not declared with the constexpr storage-class specifier”.

Proposed resolution

Put into context in 7.14.1.1 p5

5 If the signal occurs other than as the result of calling the abort or raise function, the behavior is undefined if the signal handler refers to any object with static or thread storage duration that is not a lock-free atomic object and that is not declared with the constexpr storage-class specifier other than by assigning a value to an object declared as volatile sig_atomic_t, or the signal handler calls any function in the standard library other than

Poll

  1. Does WG14 want to resolve NB comment GB-191 as indicated?

US-075 and GB-279

US-075

N3018 (the constexpr specifier for object definitions) introduces the ability to have constant expressions in more situations, but it does not update the rules for constant expression evaluation of floats. This paragraph is not possible to implement in the case of floating-point types. C++ avoids this with a recommended practice http://eel.is/c++draft/expr.const#13 because they recognized the implementation impossibilities. Consider code like:

  constexpr float f = 1.0f;
  constexpr float g = 3.0f;
  fesetround(FE_TOWARDSZERO);
  constexpr float h = f / g;

Short of performing a full analysis of the control flow graph for the translation unit, there’s no way to meet this semantic requirement.

Consider using a recommended practice for the floating-point semantic requirements instead of mandating them.

GB-279

What evaluation rules apply to initializers for objects with automatic storage duration declared with the constexpr storage-class specifier? Are the same rules applied as for static and thread storage duration (so allowing such an initializer that might raise exceptions if evaluated at execution time, e.g. “constexpr double x = (double)(1.0 / 3.0);”? Or are execution-time evaluation rules applied, resulting in a constraint violation because (double)(1.0 / 3.0) is not considered to evaluate to a constant? And does this differ depending on whether the FENV_ACCESS pragma is in effect, the FENV_ROUND or FENV_DEC_ROUND pragma is in effect, or both?

Make some suitable change (both to normative text, if any such initializers are to be accepted that would raise exceptions at execution time, and adding examples, in any case) to clarify the handling of such initializers, following any recommendations from the C floating-point group.

Re-evaluation of the initializer

Regardless of the particular solutins that the FP SG may find for the mentioned problems, this discussion shows that we have missed an important aspect of constexpr objects, namely the question if their initializers are evaluated at translation time or at each time the defining construct is met.

(JG: I would have preferred to have static storage duration for all these beasts, that would have avoided a lot of trouble. The only reason we have that is compatibility with C++)

Proposed resolution

In the context of 6.2.4 p6

6 For such an object that does not have a variable length array type, its lifetime extends from entry into the block with which it is associated until execution of that block ends in any way. (Entering an enclosed block or calling a function suspends, but does not end, execution of the current block.) If the block is entered recursively, a new instance of the object is created each time. The initial representation of the object is indeterminate. If an initialization is specified for the object and it is not specified with constexpr, it is performed each time the declaration or compound literal is reached in the execution of the block; if it is specified with constexpr the initializer is evaluated once at translation time and the new instance of the object is initialized to that fixed value each time the specification is reached; otherwise, the representation of the object becomes indeterminate each time the declaration is reached.

This discussion also sheds light on a similar problem for objects with thread storage duration that might have been dormant since their introduction in C11. These also have initializers that obey the same rules as for static, but it is not clear if they are evaluated each time a thread is started. (Hopefully nobody does that.) C17 only deals with this problem in optional floating point Annex F, not in general. So this may also need some correction in the context of 6.2.4 p4 such that implementations that implement threads but not Annex F (such as clang) are bound to these semantics.

4 An object whose identifier is declared with the storage-class specifier thread_local has thread storage duration. Its explicit or implicit initializer is evaluated prior to program execution, its lifetime is the entire execution of the thread for which it is created, and its stored value is initialized with the previously determined value when the thread is started. There is a distinct object per thread, and use of the declared name in an expression refers to the object associated with the thread evaluating the expression. The result of attempting to indirectly access an object with thread storage duration from a thread other than the one with which the object is associated is implementation-defined.

Polls

  1. Does WG14 want to apply the proposed resolution to US-075 and GB-279 for automatic storage duration?

  2. Does WG14 want to apply the proposed resolution also for thread storage duration?