Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Toying around with literal, non-type template parameters in c++20, I found out that g++ and clang++ disagree about the following code.

#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);

See it live on godbolt: https://godbolt.org/z/f3afjd

First and foremost, I'm not even sure I'm doing it the right way: is it that the way one matches with a generic StringLiteral<N> type, or is it not? If not, what's the right way?

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


EDIT: found out that removing the size_t N parameter in the partial specialization makes both compilers agree, and the result is the expected one. Like this:

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

However, I'm still curious about whether my 1st attempt is legit, by the standard, and which compiler got it wrong.

like image 939
Fabio A. Avatar asked Mar 21 '21 10:03

Fabio A.


1 Answers

Let's focus on the partial specialization of is_named:

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

and particularly try to answer whether it violates [temp.class.spec.match]/3 or not:

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.

noting that Clang apparently does not think so, and uses the single template argument to the primary template to deduce all template arguments of the partial specialization. In this particular case these template arguments are those matching its template-parameter-list:

  • the template argument for the type template parameter T
  • the template argument for the non-type template parameter N
  • the template argument for the non-type template parameter Name

As per [temp.deduct.type]/4:

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

we can break the question down into whether all three template parameters T, N and Name of the partial specialization are used in least one deduced context or not.

Starting from [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.

and [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.

We can without loss of generality consider the actual type of the template argument for the single type template parameter of the primary template (which we intend to be part fo the family of types for which partial specialization to applies), say A, as named<int, StringLiteral<5>{"ciao"}>.

The type specified in terms of the template parameters of the partial specialization, say P, is named<T, Name>.

  • T can be trivially deduced, matching A/P as named<int, StringLiteral<5>{"ciao"}>/named<T, Name>, to int, as T in named<T, Name> is not in a non-deduced context.
  • Name is similarly not in a non-deduced context, and can be deduced to StringLiteral<5>{"ciao"}.
  • The tricky part is N, which is not explicitly part of P, but only implicitly so via the template parameter Name. However, here we may simply apply the deduction rules recursively: Name has been deduced to StringLiteral<5>{"ciao"}, meaning we consider a new A/P pair StringLiteral<5>/StringLiteral<N>, in which N is non in a non-deducible context, and N can thus ultimately be deduced to 5.

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

Consequently, Clang (as well as MSVC) are correct to accept your original variant, whereas GCC is wrong to reject it (rejects-valid bug).

A more minimal example which is (correctly) accepted by Clang and MSVC, and (incorrectly) rejected by GCC is:

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

and I have filed a bug report using this example:

  • Bug 99699 - Type deduction failure for deducing a non-type template parameter via another deducible structural type (class template specialization) non-type template parameter

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

In your second variant, the deduction case is not as complex as the first one, and you can use a similar analysis to see that is likewise well-formed (all template parameters of the partial specialization are deducible).

like image 149
dfrib Avatar answered Oct 23 '22 09:10

dfrib