Why does "&& true" added to a constraint make a function template a better overload?

Viewed 952

Consider the following two overloads of a function template foo:

template <typename T>
void foo(T) requires std::integral<T> {
    std::cout << "foo requires integral\n";
}

template <typename T>
int foo(T) requires std::integral<T> && true {
    std::cout << "foo requires integral and true\n";
    return 0;
}

Note the difference between the two constraints: the second has an extra && true.

Intuitively speaking, true is redundant in a conjunction (since X && true is just X). However, it looks like this makes a semantic difference, as foo(42) would call the second overload.

Why is this the case? Specifically, why is the second function template a better overload?

4 Answers

As per [temp.constr.order], particularly [temp.constr.order]/1 and [temp.constr.order]/3

/1 A constraint P subsumes a constraint Q if and only if, [...] [ Example: Let A and B be atomic constraints. The constraint A ∧ B subsumes A, but A does not subsume A ∧ B. The constraint A subsumes A ∨ B, but A ∨ B does not subsume A. Also note that every constraint subsumes itself. — end example ]

/3 A declaration D1 is at least as constrained as a declaration D2 if

  • (3.1) D1 and D2 are both constrained declarations and D1's associated constraints subsume those of D2; or
  • (3.2) D2 has no associated constraints.

if we consider A as std::integral<T> and B as true; then:

  • A ∧ B which is std::integral<T> && true subsumes A, which is std::integral<T>,

meaning that for the following declarations:

// Denote D1
template <typename T>
void foo(T) requires std::integral<T> && true;

// Denote D2
template <typename T>
void foo(T) requires std::integral<T>;

the associated constraints of D1 subsume those of D2, and thus D1 is at least as constrained as D2. Meanwhile the reverse does not hold, and D2 is not at least as constrained as D1. This means, as per [temp.constr.order]/4

A declaration D1 is more constrained than another declaration D2 when D1 is at least as constrained as D2, and D2 is not at least as constrained as D1.

that the declaration D1 is more constrained than declaration D2, and D1 is subsequently chosen as the best match by overload resolution, as per [temp.func.order]/2:

Partial ordering selects which of two function templates is more specialized than the other by transforming each template in turn (see next paragraph) and performing template argument deduction using the function type. The deduction process determines whether one of the templates is more specialized than the other. If so, the more specialized template is the one chosen by the partial ordering process. If both deductions succeed, the partial ordering selects the more constrained template (if one exists) as determined below.

The constraint std::integral<T> && true subsumes std::integral<T> and therefore "wins" according to the partial ordering of constraints rules.

To check if constraint A subsumes B ([temp.constr.order]):

1. Both constraints are brought to a disjunctive normal form. This means all || are "expanded" to their independent form.

2. Then each disjunctive clause is split into atomic clauses (smallest &&-parts).

3. The meaning of atomic clauses themselves isn't compared, they are only compared for equality.

If constraint A contains all the atomic clauses of B and some more, then A subsumes B.

See Example 1:

[Example 1: Let A and B be atomic constraints. The constraint A ∧ B subsumes A, but A does not subsume A ∧ B. The constraint A subsumes A ∨ B, but A ∨ B does not subsume A. Also note that every constraint subsumes itself. — end example]

So it doesn't matter that the additional clause is a no-op, it's there in A but not in B, and that's all there is to it really.

Intuitively speaking, true is redundant in a conjunction

Indeed! In the interest of adopting standard-speak, let's say your specializations are functionally equivalent.

Why is the second function template a better overload?

Surprise! It isn't.

The standard explicitly references your && true constraint in an example of a program that is ill-formed, no diagnostic required.

[13.5.2.3]

a program is ill-formed, no diagnostic required, when the meaning of the program depends on whether two constructs are equivalent, and they are functionally equivalent but not equivalent.

[Example 2:

template <unsigned N> void f2()
  requires Add1<2 * N>;
template <unsigned N> int f2()
  requires Add1<N * 2> && true;
void h2() {
  f2<0>();          // ill-formed, no diagnostic required:
                    // requires determination of subsumption between atomic constraints that are
                    // functionally equivalent but not equivalent
}

— end example]

Second specialization is picked because C++ uses partial ordering algorithm for picking the generic function specialization.

Partial ordering selects which of two function templates is more specialized than the other by transforming each template in turn (see next paragraph) and performing template argument deduction using the function type. The deduction process determines whether one of the templates is more specialized than the other. If so, the more specialized template is the one chosen by the partial ordering process. If both deductions succeed, the partial ordering selects the more constrained template (if one exists) as determined below.

13.5.4 Partial ordering by constraints

13.7.6.2 Partial ordering of function templates

of C++ 20 final working draft which can be found here: http://open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4861.pdf

Related