c++visual-c++language-lawyerc++-conceptspartial-specialization

Member definition of partially specialized classes


I'm trying to define member functions of partially specialized class templates, but different compilers have wildly different opinions of what I'm allowed to do and why.

Let's take this slowly and start with something that works for all main compilers (all = gcc, clang and msvc):

#include <concepts>
#include <type_traits>

template <class T>
concept Integer
= std::is_same_v<T,int> || std::is_same_v<T,unsigned int>;

template <class T>
concept FloatingPoint
= std::is_same_v<T,float> || std::is_same_v<T,double>;


template <class T>
struct Foo
{
    T get() {return 0;}
};

template <Integer T>
struct Foo<T>
{
    T get(){ return 0; }
};

template <FloatingPoint T>
struct Foo<T>
{
    T get(){ return 0; }
};

int main()
{
    Foo<char>().get();
    Foo<int>().get();
    Foo<float>().get();
}

Examples on godbolt: gcc, clang, msvc

Cool, but I want to separate declarations and definitons of the member functions. Let's move the definiton one of the specialized classes. Examples: gcc, clang, msvc. GCC and MSVC both work fine, but Clang fails to correctly match the member definition to the correct declaration.

No worries, I was never planning to use Clang anyway. Let's try separating the other specialized definition from its declaration too. Example: gcc, clang, msvc. GCC continues to deliver, Clang gives more of the same errors, and MSVC complains that I've redefined a member...

Who is right? Do I have a fundamental misconception about partial template specialization? How do I get this working on MSVC?

Edit 2023 September

Clang 17.0.1 fixed the issue and manages to compile the example. The latest MSVC still fails.


Solution

  • Who is right?

    GCC is correct to accept all the cases, whereas Clang and MSVC are wrong to reject them; out-of class-definitions of partial specializations shall be matched to their declarations by equivalent template-head:s, which includes constraints, if any.

    The template-head of the primary template of Foo is not equivalent to the template-head of the two constrained partial specializations of it, and the definition of the primary template shall therefor not conflict with out-of-class definitions to member functions of constrained specializations of it.

    Do I have a fundamental misconception about partial template specialization?

    No, not from the looks of this question.

    How do I get this working on MSVC?

    As MSVC, like Clang, seems to (currently) have problems differentiating template-head:s for out-of-class definitions (particularly shown when partially specializing a class template by means of constraints), you will need to avoid the latter whilst compiling with MSVC (for now). As there may not be an associated MSVC bug report, you may want to consider filing one.


    Details

    As per [temp.spec.partial.general]/4 a partial specialization may indeed be constrained:

    A partial specialization may be constrained ([temp.constr]). [Example 2:

    template<typename T> concept C = true;
    
    template<typename T> struct X { };
    template<typename T> struct X<T*> { };          // #1
    template<C T> struct X<T> { };                  // #2
    

    [...] — end example]

    as long as the declaration of the primary template precedes it.

    We may minimize your example into the following:

    template <typename>
    concept C = true;
    
    template <typename T>
    struct S;
    
    template <C T>
    struct S<T> { void f(); };
    
    template <C T> 
    void S<T>::f() {}
    

    which GCC accepts but Clang rejects

    prog.cc:12:12: error: out-of-line definition of f from class S<T> without definition

    void S<T>::f() {}
         ~~~~~~^
    

    meaning Clang interprets the out-of-class definition

    template <C T> 
    void S<T>::f() {}
    

    as having a template-head that is equivalent to that of the primary template, which is wrong, as the template-head includes template-parameter:s which in turn includes type-parameter:s which in turn includes type-constraints:s, if any.

    The equivalence of template-head:s is governed by [temp.over.link]/6, which includes constraints [emphasis mine]:

    Two template-heads are equivalent if their template-parameter-lists have the same length, corresponding template-parameters are equivalent and are both declared with type-constraints that are equivalent if either template-parameter is declared with a type-constraint, and if either template-head has a requires-clause, they both have requires-clauses and the corresponding constraint-expressions are equivalent.

    Finally, albeit non-normative, both Example 2 of [temp.mem] and

    [...] A member template can be defined within or outside its class definition or class template definition. A member template of a class template that is defined outside of its class template definition shall be specified with a template-head equivalent to that of the class template followed by a template-head equivalent to that of the member template ([temp.over.link]). [...]

    [ Example 2:

    template<typename T> concept C1 = true;
    template<typename T> concept C2 = sizeof(T) <= 4;
    
    template<C1 T> struct S {
      template<C2 U> void f(U);
      template<C2 U> void g(U);
    };
    
    template<C1 T> template<C2 U>
    void S<T>::f(U) { }             // OK
    template<C1 T> template<typename U>
    void S<T>::g(U) { }             // error: no matching function in S<T>
    

    — end example]

    and Example 2 of [temp.class.general]:

    [ Example 2:

    // ...
    
    template<typename T> concept C = true;
    template<typename T> concept D = true;
    
    template<C T> struct S {
      void f();
      void g();
      void h();
      template<D U> struct Inner;
    };
    
    template<C A> void S<A>::f() { }        // OK: template-heads match
    template<typename T> void S<T>::g() { } // error: no matching declaration for S<T>
    
    template<typename T> requires C<T>      // ill-formed, no diagnostic required: template-heads are
    void S<T>::h() { }                      // functionally equivalent but not equivalent
    
    template<C X> template<D Y>
    struct S<X>::Inner { };                 // OK
    

    — end example]

    show example of out-of-class definitions of member functions of constrained class template specializations.

    This is, particularly, Clang bug:

    I'm not aware whether there is a similar bug report for MSVC or not.