11

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?

Richard Critten
  • 2,138
  • 3
  • 13
  • 16
Kelemen Máté
  • 442
  • 3
  • 12
  • 6
    I haven't yet looked at the linked examples, but there's an unwritten Stackoverflow rule: any question that shows a compiler variance is worth an automatic upvote. – Sam Varshavchik Jan 11 '21 at 13:32
  • 2
    At least for clang, the ```type constraint differs in template redeclaration```-error you are facing appears to be a bug. See this [answer](https://stackoverflow.com/a/64782324/8359552). – StefanKssmr Jan 11 '21 at 13:58
  • @samv I dunno, if it is just msvc disagreeing, that would seem too easy. ;) – Yakk - Adam Nevraumont Jan 11 '21 at 14:01
  • imho including working code and godbolt links make a nice add-on, but the essential information: The broken code and the error message are not included in this question – 463035818_is_not_an_ai Jan 11 '21 at 14:10
  • 2
    Notice that godbolt provides conformance view to see all compiler at once [Demo](https://godbolt.org/z/T4MoWo). – Jarod42 Jan 11 '21 at 14:11

1 Answers1

6

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.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • 1
    Awesome and informative answer, thank you! I filed an [issue](https://developercommunity2.visualstudio.com/t/Out-of-line-definition-of-a-constrained-/1306502) as you suggested (though Microsoft's wonderful website stripped the template arguments from the code...) – Kelemen Máté Jan 11 '21 at 18:44