C++20 中非类型文字参数的模板部分特化:clang 和 gcc 不同意

Partial specialization of templates over non-type literal parameters in C++20: clang and gcc disagree

在 c++20 中玩弄 literal, non-type template parameters,我发现 g++ 和 clang++ 不同意以下代码。

#include <algorithm>

template<size_t N>
struct StringLiteral {
    constexpr StringLiteral(const char (&str)[N]) {
        std::copy_n(str, N, value);
    }
    char value[N];
};

template <typename T, StringLiteral Name>
struct named{};

template <typename T>
struct is_named: std::false_type{};

template <typename T, size_t N, StringLiteral<N> Name>
struct is_named<named<T, Name>>: std::true_type{};

// This will fail with g++
static_assert(is_named<named<int, "ciao">>::value == true);

在 Godbolt 上观看直播:https://godbolt.org/z/f3afjd

首先,我什至不确定我这样做的方式是否正确:是匹配通用 StringLiteral<N> 类型的方式,还是不是?如果不是,正确的方法是什么?

而且,为什么编译器不同意呢?谁有问题?


EDIT:发现在部分特化中删除 size_t N 参数使两个编译器都同意,结果是预期的。像这样:

template <typename T, StringLiteral Name>
struct is_named<named<T, Name>>: std::true_type{};

但是,我仍然很好奇我的第一次尝试是否符合标准,以及哪个编译器弄错了。

让我们关注is_named的偏特化:

template <typename T, size_t N, StringLiteral<N> Name>
struct is_named<named<T, Name>>: std::true_type{};

并特别尝试回答它是否违反了[temp.class.spec.match]/3

If the template arguments of a partial specialization cannot be deduced because of the structure of its template-parameter-list and the template-id, the program is ill-formed.

注意到 Clang 显然不这么认为,并使用主模板的单个模板参数来推导出偏特化的所有模板参数。在这种特殊情况下,这些模板参数是那些匹配其模板参数列表的参数:

  • 类型模板参数的模板参数T
  • 非类型模板参数的模板参数N
  • 非类型模板参数的模板参数Name

根据[temp.deduct.type]/4

[...] If a template parameter is used only in non-deduced contexts and is not explicitly specified, template argument deduction fails. [...]

我们可以将问题分解为部分特化的所有三个模板参数 TNName 是否在至少一个推导上下文中使用。

[temp.deduct.type]/1开始

Template arguments can be deduced in several different contexts, but in each case a type that is specified in terms of template parameters (call it P) is compared with an actual type (call it A), and an attempt is made to find template argument values (a type for a type parameter, a value for a non-type parameter, or a template for a template parameter) that will make P, after substitution of the deduced values (call it the deduced A), compatible with A.

[temp.deduct.type]/3 and (again) /4:

/3 A given type P can be composed from a number of other types, templates, and non-type values:

  • [...]
  • A type that is a specialization of a class template (e.g., A<int>) includes the types, templates, and non-type values referenced by the template argument list of the specialization. [...]

/4 In most cases, the types, templates, and non-type values that are used to compose P participate in template argument deduction.

我们可以在不失一般性的情况下考虑主模板的单一类型模板参数的模板参数的实际类型(我们打算将其作为应用部分特化的类型族的一部分),比如说A,如 named<int, StringLiteral<5>{"ciao"}>

根据偏特化的模板参数指定的类型,比如P,是named<T, Name>

  • T 可以简单地推导,将 A/P 匹配为 named<int, StringLiteral<5>{"ciao"}>/named<T, Name>,匹配 int,如 [= named<T, Name> 中的 13=] 不在非推导上下文中。
  • Name 同样不在非推导上下文中,可以推导为 StringLiteral<5>{"ciao"}.
  • 棘手的部分是 N,它不是 P 的显式部分,而是通过模板参数 Name 隐含的。然而,这里我们可以简单地递归地应用推导规则:Name 已被推导为 StringLiteral<5>{"ciao"},这意味着我们考虑一个新的 A/PStringLiteral<5>/ StringLiteral<N>,其中 N 在不可推导的上下文中是非的,因此 N 最终可以推导为 5.

And, why do the compilers disagree about it? Who's got the bug?

因此,Clang(以及 MSVC)接受您的原始变体是正确的,而 GCC 拒绝它是错误的(拒绝有效错误)。

一个被 Clang 和 MSVC(正确)接受,而被 GCC(错误)拒绝的更简单的例子是:

template<int N>    struct S {};
template<S s>      struct U {};
template<typename> struct V { V() = delete; };

template <int N, S<N> s>
struct V<U<s>> {};

V<U<S<0>{}>> v{};  
// Expected:   use partial specialization #1
// GCC actual: error (rejects-valid): use of deleted function

并且我已使用此示例提交错误报告:


[...] removing the size_t N parameter in the partial specialization makes both compilers agree, [...]

在你的第二个变体中,推导情况不像第一个那么复杂,你可以使用类似的分析来查看它同样是合式的(偏特化的所有模板参数都是可推导的)。