6

I have a template class A<T> and its specialization for integral arguments. And both the class and its specialization declare the method foo(), which I would like to define outside of class bodies:

#include <concepts>

template<class T> 
struct A { static void foo(); };

template<std::integral T>
struct A<T> { static void foo(); };

template<class T>
void A<T>::foo() {}

template<std::integral T>
void A<T>::foo() {}
 
int main() { A<int>::foo(); }

GCC accepts this code.

Clang prints the error https://gcc.godbolt.org/z/hYfYGPfMh :

error: type constraint differs in template redeclaration
template<std::integral T>

And MSVC prints errors on both method definitions:

error C3855: 'A<T>': template parameter 'T' is incompatible with the declaration
error C2447: '{': missing function header (old-style formal list?)
error C2065: 'foo': undeclared identifier

Please suggest how to define methods outside class bodies and make all the compilers happy?

Fedor
  • 17,146
  • 13
  • 40
  • 131
  • 1
    It seems to be a bug of gcc. [see marked answer in other question](https://stackoverflow.com/questions/63628752/does-a-class-templates-requires-clause-have-to-be-repeated-outside-member-defin). – Георгий Гуминов Jul 30 '21 at 10:46
  • I sincerely hope gcc is the correct one out of that bunch, and the rest get fixed, because otherwise I don't see how the user-code should be adjusted. Still, I'm not sure about chapter-and-verse. Anyway, make it a [tag:language-lawyer]-question with a side-order of feasible workaround instead? – Deduplicator Jul 30 '21 at 10:48
  • @ГеоргийГуминов the other question is about a requires clause, but there is none here – 463035818_is_not_an_ai Jul 30 '21 at 10:49
  • 1
    @ГеоргийГуминов Maybe related, but distinctly different. Might indicate gcc has more useful (and hopefully right) behavior in this case by happenstance. – Deduplicator Jul 30 '21 at 10:49
  • @ГеоргийГуминов actually the case here is more similar to the "ok" one in the second answer – 463035818_is_not_an_ai Jul 30 '21 at 10:51
  • I reported Clang bug: https://bugs.llvm.org/show_bug.cgi?id=51301 – Fedor Aug 02 '21 at 06:31
  • If you remove the definition for the (unused) unspecialized `foo` MSVC does not complain. – Spencer Apr 26 '22 at 15:47

2 Answers2

2

I am not a C++ template expert, I tried something like below

template<class T, bool = std::is_integral_v<T>>
struct A
{};

template<class T>
struct A<T, false>
{ 
   static void foo(); 
};

template<class T>
struct A<T, true> 
{ 
   static void foo(); 
};

template<class T>
void A<T,false>::foo() 
{
  std::cout << "I am working on a non-integral type" << std::endl;
}

template<class T>
void A<T, true>::foo() 
{
  std::cout << "I am working on an integral type" << std::endl;
}

int main()
{
  A<int>::foo();
  A<float> ::foo();

  return 0;
}

and the code gave me the results on MS C++ compiler

I am working on an integral type
I am working on a non-integral type
Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Nitheesh George
  • 1,357
  • 10
  • 13
  • 1
    The problem with this well-known workaraound is that it changes the primary template, which might not always be acceptable (minimal change to API, significant change to ABI), or even possible (back-compat or ownership/control). Anyway, making the second argument always bool and crafting appropriate SFINAE-conditions has broader applicability. – Deduplicator Jul 31 '21 at 00:15
2

I'm pretty sure that both the MS and Clang compilers are in error here and GCC is compiling your code correctly. Until these bugs are fixed in the other compilers, I suggest to continue using the concept pattern instead of going back to outdated methods. Simply work around the bug using an additional class:

#include <concepts>
#include <iostream>

// This is a work-around for using concept specialization of
// classes in conjunction with out-of-body definition of members.
// Only helpful for MSVC and Clang. GCC is properly compiling
// out-of-body concept specializations.

template <typename T>
class A
{
    // For MSVC ONLY: the default template seems require being empty
    // for this to work, but do fiddle around with it.
    
    // (Also works with MSVC:)
    A()                         = delete;
    A(const A&)                 = delete;
    A(A&&) noexcept             = delete;
    A& operator =(const A&)     = delete;
    A& operator =(A&&) noexcept = delete;
    ~A()                        = delete;

    // Clang and GCC can have members just fine:
    // static const char* foo();
};

// We use distinct base classes to define our concept specializations of A.
template <std::signed_integral T>
class A_Signed_Integral
{
public:
    static const char* foo();
};
template <std::unsigned_integral T>
class A_Unsigned_Integral
{
public:
    static const char* foo();
};

// And then we wrap them using the actual concept specializations of A,
// making the specializations effectivel the same class as the base class.
template <std::signed_integral T>
class A<T> :
    public A_Signed_Integral<T>
{
public:
    using A_Signed_Integral<T>::A_Signed_Integral;  // grab all ctors
    using A_Signed_Integral<T>::operator =;         // an exceptional case
};
template <std::unsigned_integral T>
class A<T> :
    public A_Unsigned_Integral<T>
{
public:
    using A_Unsigned_Integral<T>::A_Unsigned_Integral;
    using A_Unsigned_Integral<T>::operator =;
};

// Out-of-body definitions can be located to another file
template <std::signed_integral T>
inline const char* A_Signed_Integral<T>::foo()
{
    return "using A<std::signed_integral T> foo";
}

template <std::unsigned_integral T>
inline const char* A_Unsigned_Integral<T>::foo()
{
    return "using A<std::unsigned_integral T> foo";
}

int main()
{
    std::cout << A<signed long>::foo() << std::endl;
    std::cout << A<unsigned long>::foo() << std::endl;

    return 0;
}

(Tested with all three compilers and works fine: see gcc.godbolt.org)

In the future, once the bugs are fixed it should be relatively easy with search and replace to erase the base classes in favor of just using the concept specializations of A.

EDIT: Updating the example to work for MSVC, which cannot seem to make use of the default template yet.

doticu
  • 46
  • 5
  • 1
    Thanks. GCC and Clang accept your code, but MSVC prints an error: https://gcc.godbolt.org/z/6zcsabYa5 – Fedor Aug 22 '21 at 05:11
  • 1
    Ah, thanks for pointing that out @Fedor, you're quite right. It seems that MSVC has yet another limitation compared to the others. I updated the example code which I verified is compiling with the MS compiler. – doticu Aug 22 '21 at 15:31
  • 1
    Thanks, I think you can simply declare `template class A;` without defining it: https://gcc.godbolt.org/z/Karedb7oz – Fedor Sep 02 '21 at 07:02
  • The problem still exists in MSVC 17.1.5, but if you remove the definition for the (unused) unspecialized `foo` there is no compiler error. – Spencer Apr 26 '22 at 15:45