cplusplus / CWG

Core Working Group
24 stars 7 forks source link

[temp.constr.atomic] p2 The meaning "same appearance of the same expression" is confusing #284

Open xmh0511 opened 1 year ago

xmh0511 commented 1 year ago

Full name of submitter (unless configured in github; will be published with the issue): Jim X

The original issue is in https://github.com/cplusplus/draft/issues/2554, which is tagged with CWG.

Two atomic constraints, e1 and e2, are identical if they are formed from the same appearance of the same expression

Consider such two contrasting examples:

struct A{};

template<class T>
requires (std::is_same_v<T,A> &&  sizeof(T)>0)
void fun(T); // #1

template<class T>
requires (std::is_same_v<T,A>)
void fun(T);  // #2

int main() {
    fun(A{});  // ambiguous
}

After a bit of modification to this example:

struct A{};
template<class T>
concept same_with_A = std::is_same_v<T,A>;
template<class T>
concept size_over_zero = sizeof(T)>0;
template<class T>
requires (same_with_A<T> && size_over_zero<A>)
void fun(T);  // #1

template<class T>
requires (same_with_A<T>)
void fun(T);  // #2

int main() {
    fun(A{});  // #1
}

In this example, the call is resolved to #1. However, in both examples, the atomic constraints are not formed from the same expressions. According to the grammar

template < template-parameter-list > requires-clause

requires-clause:

  • requires constraint-logical-or-expression

The constraint-logical-or-expressions that are expressions in two declarations are different expressions.

Suggested resolution

As exposed in the first example, even though std::is_same_v<T,A> has the same sequence of tokens, the atomic constraints formed from them are not considered to be identical. As exposed in the second example, the atomic constraints formed from same_with_A<T> are considered to be identical even though same_with_A<T> are in the different constraint-logical-or-expressions. Presumably, together with the examples in [temp.constr.atomic] example-1, the intent may want to say the atomic constraints are identical if and only if they are formed from the concepts whose constraint-expression are identical.

Two atomic constraints, e1 and e2, are identical if and only if they are formed from the same concept or those concepts whose constraint-expressions in their concept-definitions are identical.

zygoloid commented 1 year ago

Basing atomic constraint identity on the lexically enclosing declaration makes sense to me. However, if we only use the lexically enclosing declaration and not also "the same appearance of the same expression" then we will change behavior for examples such as this:

template<typename T, typename U, typename V> concept Z =
  (A<T> && (sizeof(V) == 1)) || (A<U> && (sizeof(V) == 1));

Here, Z<A, B, C> does not subsume Z<B, A, C> nor vice versa, because the atomic constraints are not identical, but if we treated the two atomic constraints as identical because they are equivalent and appear within the same concept-definition, we would treat both as subsuming each other. So this is at least not an editorial change (though maybe CWG would be interested in it as an alternative rule).

I didn't understand the second part of the suggestion -- "those concepts whose constraint-expressions in their concept-definitions are identical. That sounds like it would cause these two concepts to be equivalent, which is not the design intent:

// Should be two distinct atomic constraints.
template<typename T> concept A = true;
template<typename T> concept B = true;
xmh0511 commented 1 year ago

Here, Z<A, B, C> does not subsume Z<B, A, C> nor vice versa, because the atomic constraints are not identical

I think the subsequent wording of [temp.constr.atomic] p2 can cover this case:

given a hypothetical template A whose template-parameter-list consists of template-parameters corresponding and equivalent ([temp.over.link]) to those mapped by the parameter mappings of the expression, a template-id naming A whose template-arguments are the targets of the parameter mapping of e1 is the same ([temp.type]) as a template-id naming A whose template-arguments are the targets of the parameter mapping of e2.

A<T1, T2, T3> is not the same as A<T2, T1, T3>.

I didn't understand the second part of the suggestion -- "those concepts whose constraint-expressions in their concept-definitions are identical. That sounds like it would cause these two concepts to be equivalent, which is not the design intent:

I thought the wording could cover

template <unsigned N> constexpr bool Atomic = true;
template <unsigned N> concept C = Atomic<N>;
template <unsigned N> concept Add1 = C<N + 1>;
template <unsigned N> concept AddOne = C<N + 1>;

template <unsigned M> void f()
requires Add1<2 * M>;

template <unsigned M> int f()
requires AddOne<2 * M> && true;

Yes, the wording would accidentally cover

// Should be two distinct atomic constraints.
template<typename T> concept A = true;
template<typename T> concept B = true;

Which is not the intent of the standard.

Two atomic constraints, e1 and e2, are identical if and only if they are formed from the same concept and if, given a hypothetical template A...

This may be sufficient to cover the case

template<class T>
concept D = true;
template<typename T> concept A = D<T> && true;
template<typename T> concept B = D<T>;

The constraints formed from A<T> and B<T> would be X ∧ Y and X where X is the same atomic constraint.