约束重载如何通过概念的部分排序解决?

问题描述

对于每个cppreference,使用约束对约束的局部排序来确定“模板模板参数的最佳匹配”。并且在示例部分中,选择了“更受约束”的选项,即更强/更严格的条件。但是在实践中,当对功能概念的约束进行明确排序并且特定的调用可以清楚地区分最强的选择时,我会发现一些令人困惑的行为。

我正在gcc版本10.2.0(Homebrew GCC 10.2.0)上测试以下内容:With:

template <typename... Ts>
void foo (Ts... ts) // option 1
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires (sizeof...(Ts)<=3)
void foo (Ts... ts) // option 2
{
    (cout << ... << ts) << endl;
}

调用foo(1,2)会选择选项2,因为它的约束显然比选项1强。另一方面,这显然会引起歧义:

template <typename... Ts>
void foo (Ts... ts) // option 1
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires (sizeof...(Ts)<=3)
void foo (Ts... ts) // option 2
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires (same_as<Ts,int> && ...)
void foo (Ts... ts) // option 3
{
    (cout << ... << ts) << endl;
}

因为调用foo(1,2)无法决定选择选项2还是选择3,因为它们是无与伦比的。现在,如果我正确理解的话,可以添加以下类似的合取案子:

template <typename... Ts>
void foo (Ts... ts) // option 1
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires (sizeof...(Ts)<=3)
void foo (Ts... ts) // option 2
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires (same_as<Ts,int> && ...)
void foo (Ts... ts) // option 3
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires (sizeof...(Ts)<=3) && (same_as<Ts,int> && ...)
void foo (Ts... ts) // option 4
{
    (cout << ... << ts) << endl;
}

调用foo(1,2)应该可以通过选项4解决,但是我的编译器则表示其他方式:

In function 'int main()':
error: call of overloaded 'foo(int,int)' is ambiguous
      |     foo(1,2);
      |            ^
note: candidate: 'void foo(Ts ...) [with Ts = {int,int}]'
      | void foo (Ts... ts) // option 1
      |      ^~~
note: candidate: 'void foo(Ts ...) [with Ts = {int,int}]'
      | void foo (Ts... ts) // option 2
      |      ^~~
note: candidate: 'void foo(Ts ...) [with Ts = {int,int}]'
      | void foo (Ts... ts) // option 3
      |      ^~~
note: candidate: 'void foo(Ts ...) [with Ts = {int,int}]'
      | void foo (Ts... ts) // option 4

那是为什么?而且如果不可避免,有什么办法可以解决

解决方法

如果您尝试编译:

template <typename... Ts> requires (sizeof...(Ts)<=3)
void foo (Ts... ts) // option 2
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires (sizeof...(Ts)<=3) && (is_same_v<Ts,int> && ...)
void foo (Ts... ts) // option 4
{
    (cout << ... << ts) << endl;
}

通过clang 12,您收到以下消息:

note: similar constraint expressions not considered equivalent; 
constraint expressions cannot be considered equivalent unless 
they originate from the same concept

有了这个提示,您可以将代码重写为:

#include <iostream>
#include <type_traits>

using namespace std;
template <typename... Ts>
void foo (Ts... ts) // option 1
{
    (cout << ... << ts) << endl;
}

template<class ... Ts>
concept LessThan3 =  (sizeof...(Ts)<=3)  ;

template<class ... Ts>
concept AllInt =  (std::same_as<Ts,int> && ...);


template <typename... Ts> requires LessThan3<Ts...>
void foo (Ts... ts) // option 2
{
    (cout << ... << ts) << endl;
}


template <typename... Ts> requires AllInt<Ts...>
void foo (Ts... ts) // option 3
{
    (cout << ... << ts) << endl;
}

template <typename... Ts> requires LessThan3<Ts...> &&  AllInt<Ts...>
void foo (Ts... ts) // option 4
{
    (cout << ... << ts) << endl;
}

int main(){
   
    foo(1,2);
    return 0;
}

使用gcc和clang进行编译:demo

,

当编译器检查一组需求是否比另一组受到更多约束时,它将递归扩展概念。它还了解&&||( )的含义(因此,合取/分取的顺序无关紧要,多余的( )无关紧要,等等) ,甚至在概念内部。

这部分非常直观。不直观的是,两个条件在词汇上是相同的还不足以使它们被认为是等效的。在扩展概念之前,它们在字面上必须在源代码中的相同位置处是相同的表达式。这要求它们起源于相同的概念。

另一个非直觉的部分是&&||在折叠表达式中失去其特殊含义,因此对于两个折叠表达式而言,它们被认为是等效的(无论它们使用&&还是{{ 1}}等),在扩展概念之前,它们也必须位于源代码中的同一位置。

知道了这一点,解决方案是将||sizeof...(Ts) <= 3抽象为概念。

有很多方法可以做到这一点。您可以根据自己的意愿进行一般或特定的操作:

  1. (same_as<Ts,int> && ...)

    用法:template <typename ...P> concept at_most_3 = sizeof...(P) <= 3; template <typename ...P> concept all_ints = (std::same_as<P,int> && ...);

  2. requires at_most_3<Ts...> && all_ints<Ts...>

    用法:template <auto A,auto B> concept less_eq = A <= B; template <typename T,typename ...P> concept all_same_as = (std::same_as<T,P> && ...);

甚至被用作requires less_eq<sizeof...(Ts),3> && all_same_as<int,Ts...>的完全template <bool X> concept boolean = X;似乎也行得通!