与模板化结构或 class 结合使用的概念的正确语法是什么?

What is the correct syntax for concept in combination with a templated struct or class?

最近我在使用 concepts 为 templated struct 定义不同的构造函数。这是代码:

#include <iostream>

namespace detail {
    template<typename T, typename U >
    concept SameHelper = std::is_same_v<T, U>;
}

template<typename T, typename U >
concept same_as = detail::SameHelper<T, U> && detail::SameHelper<U, T>;

template<typename T>
concept trivial = same_as<T, bool> || same_as<T, char>;

template<typename T>
concept not_trivial = !trivial<T>;

template<typename T>
struct Foo
{
    Foo(T t) requires trivial<T> : member{t} { std::cout << "Foo is trivial" <<std::endl; }
    Foo(const auto& t) requires not_trivial<T> : member{t} { std::cout << "Foo is not trivial" <<std::endl;}
    const T member;
};

template<typename T>
struct Bar
{
    Bar(auto t) requires trivial<T> : member{t} { std::cout << "Bar is trivial" <<std::endl; }
    Bar(const T& t) requires not_trivial<T> : member{t} { std::cout << "Bar is not trivial" <<std::endl;}
    const T member;
};

template<typename T>
struct Baz
{
    Baz(auto t) requires trivial<T> : member{t} { std::cout << "Baz is trivial" <<std::endl; }
    Baz(const auto& t) requires not_trivial<T> : member{t} { std::cout << "Baz is not trivial" <<std::endl;}
    const T member;
};

template<typename T>
struct Qux
{
    Qux(T t) requires trivial<T> : member{t} { std::cout << "Qux is trivial" <<std::endl; }
    Qux(const T& t) requires not_trivial<T> : member{t} { std::cout << "Qux is not trivial" <<std::endl;}
    const T member;
};

int main()
{
    Foo(true);
    Foo(3.14159);

    Bar(true);
    Bar(3.14159);

    //Baz(true);    // does not compile if uncommented
    //Baz(3.14159); // does not compile if uncommented

    //Qux(true);    // does not compile if uncommented
    //Qux(3.14159); // does not compile if uncommented

    return 0;
}

You can run the above code online。 我想知道为什么 Foo 和 Bar 编译得很好,而 Baz 和 Qux 如果没有注释就不能编译。 恕我直言,Baz 和 Qux 的语法更方便。

让我们按顺序浏览所有 class 模板。我将使用一个更简单的概念,因为 bool 是唯一相关的类型:

template <typename T>
struct Foo
{
    Foo(T) requires same_as<T, bool>;
    Foo(const auto&) requires (not same_as<T, bool>);
};

Foo(true);
Foo(3.14159);

在进行 class 模板参数推导时,过程是我们获取所有构造函数并将它们转换为函数 - 然后执行重载解析以找出我们最终的具体类型。对于 Foo,这些将变为:

template <typename T> requires same_as<T, bool>
auto __f(T) -> Foo<T>;

template <typename T> requires (not same_as<T, bool>)
auto __f(const auto&) -> Foo<T>;

__f(true);    // from Foo(true)
__f(3.14159); // from Foo(3.14159)

__f 的第一个重载中,T 可以从其参数中推导出来。在第二次超载中,它不是 - 无法确定 T 是什么......所以就 CTAD 流程而言,它基本上无关紧要。结果,__f(true) 没问题(你返回 Foo<bool>)但 __f(3.14159) 格式错误 - 第一个重载不可行,因为 double 不是 bool 并且第二个重载不可行,因为未推导出 T

至少规则应该是这样。今天存在的措辞缺少我们将约束从每个构造函数转移到重载集中的规则,而 clang 恰好遵循这里的规则字母 - 它的 __f 版本没有附加到它们的任何 requires。但这绝对不是我们想要在这里发生的事情,而且肯定会成为核心问题。另见 llvm bug #44484

Bar 类似,只是参数翻转:

template<typename T>
struct Bar
{
    Bar(auto) requires same_as<T, bool>;
    Bar(const T&) requires (not same_as<T, bool>);
};

在这里,唯一可以为我们提供 CTAD 答案的构造函数是第二个 - 但第二个构造函数要求 T 不是 bool。所以 Bar(true) 格式错误,但 Bar(3.14159) 没问题,给你 Bar<double>.

对于Baz

template<typename T>
struct Baz
{
    Baz(auto) requires same_as<T, bool>;
    Baz(const auto&) requires (not same_as<T, bool>);
};

现在 构造函数都没有参与 CTAD,所以你必须自己编写一个推导指南才能在这里做任何事情。拒绝这些是正确的。

Qux

template<typename T>
struct Qux
{
    Qux(T) requires same_as<T, bool>;
    Qux(const T&) requires (not same_as<T, bool>);
};

在这里,两个构造函数 do 都参与了 CTAD,因此 Qux(true)Qux(3.14159) 都工作正常(只是每个选择不同的构造函数)。这与我们之前看到的行为相同——clang 遵循它们本来的规则,而 gcc(和 msvc)遵循它们应该遵循的规则。