Skip to content

[temp.constr.atomic] if an atomic constraint is made by a requires-expression, invalid types/expressions in its requires-seq should not directly affect the satisfaction of the constraint itself #675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ingmaurorusso opened this issue Feb 24, 2025 · 4 comments

Comments

@ingmaurorusso
Copy link

ingmaurorusso commented Feb 24, 2025

Reporter:
mauro russo

Reference (section label): Reference (section label):
temp.constr.atomic - p3

Link to reflector thread (if any):
None

Issue description:

As discussed on std-discussion mailing list (from 2882 to 2887),
the standard might be more precise when discussing about invalid types / expressions in an atomic constraint after the substitutions.

In particular, if the expression of atomic constraint is a requires-expression, then the requires-expression is valid (and eventually satisfied) even if its body (requires-seq) contains any invalid types/expression.
However, the text of temp.constr.atomic - p3 generically reads:
"... If substitution results in an invalid type or expression in the immediate context of the atomic constraint (...), the constraint is not satisfied. ... ".

Since no defintion or note or other clause excludes the requires-seq from what has to be considered as part of the immediate context here, then we fall in a lack of consistency compared to the text of expr.prim.req.general - p5 as it allows the expression to evaluate as true even if some invalid type or expression is within the sequence of its requirements.

In 2882 there is an example where the incosistency is in place, but here I prefer to report the nicer example from Andrew Schepler ( 2887 ):

template<typename T>
requires ( ! requires { T::value; } )
int func() { return 0;}

int a = func<int>();

Such a code is accepted by major compilers ( godbolt ) despite int::value is an invalid expression in the atomic constraint. This appears to be a fair behaviour, because any requires-expression should represent (IMHO) a boundary between different levels of a constraint hierarchy.
Such a boundary is already expressed for a nested requirement inside a requires-expression ( expr.prim.req.nested - p1 ).

Suggested resolution:

A note similar to temp.deduct.general - p9 (related to the body of a lambda-expression as not part of the immediate context) should help.
However, such a similar note alone would risk to consider invalid expressions inside the body of a requires-expression as an error in non-immediate context, which is even worse.

Therefore, some stronger boundary separation in temp.constr.atomic - p3 should be considered, exactly as in expr.prim.req.nested - p1 .
For example, add after "If substitution results in an invalid type or expression in the immediate
context of the atomic constraint (13.10.3.1), the constraint is not satisfied.
" the words "The validity of a requires-expression is not affected by the invalid types or expressions inside its requirements which only get involved in the constant evaluation of such expression as specified in (7.5.8)."

In addition, Andrew Schepler, in 2887 , remarkably suggested to enrich also the parts temp.deduct.general - p7 - Note 4 and temp.deduct.general - p8

His original comment on it reads:
I'd consider adding to the note in [temp.deduct.general]/7, which currently explains how a function's noexcept-specifier is instantiated, instead of during the TAD substitutions in the "deduction substitution loci". A template's constraints are a similar thing closely associated with the template declaration, but as you're clarifying, the substitution happens in a different way. Although in this case checking constraints always happens immediately after the TAD substitution (paragraph 5), it would still make clear that paragraph 8 "If a [TAD] substitution results in an invalid type or expression..." does not apply.

In conclusion, due to the presence of multiple points where it would be needed to reuse the idea of ignoring the invalid types or expressions in the body of a requires-expression, for any aim in the contexts including the requires-expression itself (other than its true/false constant evaluation), the best solution is likely to enrich the clause expr.prim.req.general, by adding a new part 6,
as proposed in the PR 7712
or, better, in this local branch.

@ingmaurorusso ingmaurorusso changed the title [temp.constr.atomic] if an atomic constraint is made by a requires-expression, invalid types/expressions in its requires-seq should not affect the satisfaction of the constraint itself [temp.constr.atomic] if an atomic constraint is made by a requires-expression, invalid types/expressions in its requires-seq should not directly affect the satisfaction of the constraint itself Feb 24, 2025
@frederick-vs-ja
Copy link

Can we just say something like the following (in [temp.constr.atomic] p3)?

For the purpose of validness determination, every type or expression in a requirement of a requires-expression is ignored.

@ingmaurorusso
Copy link
Author

yes, it would a viable solution to me, except that this way only the atomic constraints case would be covered,
whereas other parts as temp.deduct.general - p7 - Note 4 and temp.deduct.general - p8 would remain not covered.

I also initially supposed to suggest an extension of temp.constr.atomic - p3 only, but from the thread on std-discussion they noticed other points might be involved.

@frederick-vs-ja
Copy link

yes, it would a viable solution to me, except that this way only the atomic constraints case would be covered,
whereas other parts as temp.deduct.general - p7 - Note 4 and temp.deduct.general - p8 would remain not covered.

Hmm, I think there should be a unified source of such validness, and the amendment for require-expressions should be placed there. Is there already one?

@jensmaurer
Copy link
Member

I'm getting slightly confused by the wall of text here. Let's get back to the original example, slightly amended:

template<typename T>
requires T::value1 || (! requires { T::value2; } )
int func() { return 0;}

int a = func<int>();

This has a disjunction of two atomic constraints. When we instantiate func<int>, we check satisfaction of the associated constraints. We apply [temp.constr.atomic]p3 to the atomic constraint T::value1 and get a "not satisfied" result, because int::value1 is not a valid expression. So far, so good. We then apply [temp.constr.atomic]p3 to the atomic constraint (! requires { T::value2; } ), and that seems to do deep substitution right away, yielding int::value2 as an invalid expression, so the atomic constraint apparently is not satisfied.

That seems undesirable; we want the phrasing from nested-requirements for all requires-expressions:

Substitution of template arguments into a requires-expression does not result in substitution into the requirement-body other than as specified in (where?).

Then, we're missing rules how the checking of the requirement-body actually works. We probably want checking in lexical order, and we want some "shall be satisfied" for each requirement in the requirement-body. Then, for each item that can appear there, we should say how the "satisfied" check works (first substitute, then check for invalid types or expressions). "A simple-requirement asserts the validity of an expression." is too imprecise.

Does that sound reasonable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants