Search code examples
c++c++20variadic-templatesc++-conceptscompiler-bug

c++20 partial class specialization with concepts and variadic template args


Looking to understand why the code below results in:

class template partial specialization is not more specialized than the primary template

template <typename T, std::integral... Us>
struct foo : std::false_type {};

template <std::integral T, std::integral... Us>
struct foo<T, Us...> : std::true_type {};

Clearly it's more specialized based on the first template argument, regardless whether remaining variadic arguments are contrained via std::integral concept.

Version 2 - no contraints on variadic args

In fact, if we remove the contraints from Us the specialization takes place as expected:

template <typename T, typename... Us>
struct foo : std::false_type {};

template <std::integral T, typename... Us>
struct foo<T, Us...> : std::true_type {};

Version 3 - single arg with contraint

The same is true if we use a single U, even with the contraint present:

template <typename T, std::integral U>
struct foo : std::false_type {};

template <std::integral T, std::integral U>
struct foo<T, U> : std::true_type {};

Version 4 - workaround

I have found a workaround that is functionally equivalent to the code in question:

template <typename T, std::integral... Us>
struct foo : std::false_type {};

template <std::integral T>
struct foo<T> : std::true_type {};

template <std::integral T, std::integral U, std::integral... Us>
struct foo<T, U, Us...> : std::true_type {};

godbolt link for experimantation with code above: https://godbolt.org/z/9Tn1KeKnK

Is this a bug?


Solution

  • You have run into a compiler bug which surprisingly both GCC and clang have. The relevant wording is this:

    A declaration D1 is more constrained than another declaration D2 when D1 is at least as constrained as D2, and D2 is not at least as constrained as D1.

    - [temp.constr.order] more constrained

    Furthermore, [temp.func.order] p6.4 states:

    Otherwise, if one template is more constrained than the other, the more constrained template is more specialized than the other.

    The full definition of "more constrained" is somewhat involved, but obviously, having the constraint std::integral<T> makes the partial specialization more constrained than the primary template, which has no such constraint.

    More formally, the disjunctive normal(1) form of the primary template constraint would be(2):

    (std::integral<U> && ...) // (3)
    

    The disjunctive normal form(1) of the partial specialization constraint would be:

    (std::integral<U> && ...) && std::integral<T>
    

    The latter DNF is the former DNF with one additional constraint, therefore, the latter subsumes the former. The partial specialization is more constrained, and therefore more specialized.


    (1) In this case, the DNF is simultaneously the conjunctive normal form because there is no disjunction.

    (2) This is based on [temp.param] p5 and [temp.constr.decl] p3.

    (3) Interestingly, the fold expression in this DNF is considered to be a single atomic constraint. This detail does not change anything about the answer though.

    Compiler bugs

    Note that MSVC compiles version 1 just fine, as it should. See Compiler Explorer. Only GCC and clang fail to compile it.

    None of your workarounds (versions 2 through 4) should be necessary. Note that the behavior or GCC and clang is uttely nonsensical. Even if we add tons of additional constraints, such as

    requires std::floating_point<T> && (std::floating_point<Us> && ...)
    

    ... GCC and clang still do not consider this partial specialization to be more constrained. See Compiler Explorer.

    The clang diagnostics hint at a possible cause for this bug:

    <source>:14:28: note: similar constraint expressions not considered > equivalent; constraint expressions cannot be considered equivalent unless they originate from the same concept
       14 | template <std::integral T, std::integral... Us>
          |                            ^~~~~~~~~~~~~~~~
    <source>:11:23: note: similar constraint expression here
       11 | template <typename T, std::integral... Us>
          |                       ^~~~~~~~~~~~~~~~
    

    This is presumably because such a type-constraint turns int a fold expression, see Concepts TS Issue 28. However, regardless of what std::integral... is considered to be, the additional constraint std::integral<T> should make the partial specialization more constrained.

    Best workaround yet

    By avoiding the fold expression issue, which results in utterly nonsensical behavior, we can get GCC and clang to behave well:

    template <typename... Ts>
    concept all_integral = (std::integral<Ts> && ...);
    
    template <typename T, typename... Us>
    requires all_integral<Us...>
    struct foo : std::false_type {};
    
    template <std::integral T, typename... Us>
    requires all_integral<Us...>
    struct foo<T, Us...> : std::true_type {};
    

    Perhaps the reason why this works is that GCC and clang otherwise incorporate the constraint on T into the fold expression for Us, and the entire constraint is thus atomic.