How to define a specialized class method outside of class body in C++?

Viewed 292

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?

2 Answers

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

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.

Related