约束包含是否仅适用于概念?

Does constraint subsumption only apply to concepts?

考虑这个例子:

template <typename T> inline constexpr bool C1 = true;    
template <typename T> inline constexpr bool C2 = true;

template <typename T> requires C1<T> && C2<T> 
constexpr int foo() { return 0; }

template <typename T> requires C1<T> 
constexpr int foo() { return 1; }

constexpr int bar() {
    return foo<int>();
}

调用 foo<int>() 是否含糊不清,或者约束 C1<T> && C2<T> 包含 C1<T>

是的。 只能包含 个概念。对 foo<int> 的调用是不明确的,因为两个声明都不是 "at least as constrained as" 另一个。

但是,如果 C1C2 都是 concept 而不是 inline constexpr bool,那么 foo() 的声明 returns 0 至少与 foo() 声明 returns 1 一样受到约束,并且对 foo<int> 的调用是有效的和 return 0。这是更喜欢使用概念作为对任意布尔常量表达式的约束的原因之一。


背景

这种差异的原因(概念包含,任意表达式不包含)在Semantic constraint matching for concepts中得到了最好的表达,值得全文阅读(我不会在这里重复所有论点)。但以论文为例:

namespace X {
  template<C1 T> void foo(T);
  template<typename T> concept Fooable = requires (T t) { foo(t); };
}
namespace Y {
  template<C2 T> void foo(T);
  template<typename T> concept Fooable = requires (T t) { foo(t); };
}

X::Fooable is equivalent to Y::Fooable despite them meaning completely different things (by virtue of being defined in different namespace). This kind of incidental equivalence is problematic: an overload set with functions constrained by these two concepts would be ambiguous.

That problem is exacerbated when one concept incidentally refines the others.

namespace Z {
  template<C3 T> void foo(T);
  template<C3 T> void bar(T);
  template<typename T> concept Fooable = requires (T t) {
    foo(t);
    bar(t);
  };
}

An overload set containing distinct viable candidates constrained by X::Fooable, Y::Fooable, and Z::Fooable respectively will always select the candidate constrained by Z::Fooable. This is almost certainly not what a programmer wants.


标准参考文献

包含规则在[temp.constr.order]/1.2:

an atomic constraint A subsumes another atomic constraint B if and only if the A and B are identical using the rules described in [temp.constr.atomic].

原子约束在 [temp.constr.atomic]:

中定义

An atomic constraint is formed from an expression E and a mapping from the template parameters that appear within E to template arguments involving the template parameters of the constrained entity, called the parameter mapping ([temp.constr.decl]). [ Note: Atomic constraints are formed by constraint normalization. E is never a logical AND expression nor a logical OR expression. — end note ]

Two atomic constraints are identical if they are formed from the same expression and the targets of the parameter mappings are equivalent according to the rules for expressions described in [temp.over.link].

这里的关键是形成了原子约束。这是这里的关键点。在 [temp.constr.normal]:

The normal form of an expression E is a constraint that is defined as follows:

  • The normal form of an expression ( E ) is the normal form of E.
  • The normal form of an expression E1 || E2 is the disjunction of the normal forms of E1 and E2.
  • The normal form of an expression E1 && E2 is the conjunction of the normal forms of E1 and E2.
  • The normal form of an id-expression of the form C<A1, A2, ..., An>, where C names a concept, is the normal form of the constraint-expression of C, after substituting A1, A2, ..., An for C's respective template parameters in the parameter mappings in each atomic constraint. If any such substitution results in an invalid type or expression, the program is ill-formed; no diagnostic is required. [ ... ]
  • The normal form of any other expression E is the atomic constraint whose expression is E and whose parameter mapping is the identity mapping.

对于foo的第一个重载,约束是C1<T> && C2<T>,所以要规范化它,我们得到C1<T>1的范式的合取C2<T>1 然后我们就完成了。同样,对于 foo 的第二个重载,约束是 C1<T>2,这是它自己的范式。

使原子约束相同的规则是它们必须由相同的表达式(源代码级构造)形成。虽然这两个函数都具有使用标记序列 C1<T> 的原子约束,但它们在源代码中并不相同 文字表达式

因此下标表明这些实际上不是同一个原子约束。 C1<T>1C1<T>2 不同。规则不是令牌等价!所以第一个 fooC1<T> 不包含第二个 fooC1<T>,反之亦然。

因此,模棱两可。

另一方面,如果我们有:

template <typename T> concept D1 = true;    
template <typename T> concept D2 = true;

template <typename T> requires D1<T> && D2<T> 
constexpr int quux() { return 0; }

template <typename T> requires D1<T> 
constexpr int quux() { return 1; }

第一个函数的约束是 D1<T> && D2<T>。第三个项目符号给了我们 D1<T>D2<T> 的结合。第 4 个项目符号然后引导我们代入概念本身,因此第一个项目符号归一化为 true1,第二个归一化为 true2 。同样,下标表示 引用了哪个 true

第二个函数的约束是 D1<T>,它将(第 4 个项目符号)规范化为 true1.

而现在,true1确实和true1是同一个表达式,所以这些约束被认为是相同的。结果,D1<T> && D2<T> 包含 D1<T>,而 quux<int>() 是一个明确的调用 returns 0.