Mag_conv
Mag_conv

Reputation: 21

std::enable_if for partial specialization

I am implementing a template Matrix class (depending on the underlying datatype and the size) and want to specialize some methods (determinant, inverse) for some specific sizes (2, 3 and 4).

I first tried to implement the class as follow:

#include <type_traits>
#include <iostream>

template <typename T, std::size_t N>
class Matrix
{
  public:
    Matrix() = default;

  // [...]

  template <typename = std::enable_if_t<N == 2>>
  T determinant() const
  {
    T det = 0;
    // [...]
    std::cout << "Determinant for 2x2 matrix" << std::endl;
    return det ;
  }

  template <typename = std::enable_if_t<N == 3>>
  T determinant() const
  { 
    T det = 0;
    // [...]
    std::cout << "Determinant for 3x3 matrix" << std::endl;
    return det ;
  }


  private:
    T data[N*N];
};

int main()
{
  Matrix<int, 3> mat1;
  mat1.determinant();
}

It was compiling when only the method for N==2 was implemented, but then when I implemented the method for N==3, I got the compilation error 'determinant(void) const': member function already defined or declared.

<source>:22:5: error: 'template<class T, long unsigned int N> template<class> T Matrix<T, N>::determinant() const' cannot be overloaded with 'template<class T, long unsigned int N> template<class> T Matrix<T, N>::determinant() const' [-Wtemplate-body]
   22 |   T determinant() const
      |     ^~~~~~~~~~~
<source>:13:5: note: previous declaration 'template<class T, long unsigned int N> template<class> T Matrix<T, N>::determinant() const'
   13 |   T determinant() const
      | 
[... and more ...]  

Why ?

I then achieved the desired behavior by modifying the implementation of my class as follow:

#include <type_traits>
#include <iostream>

template <typename T, std::size_t N>
class Matrix
{
  public:
    Matrix() = default;

    // [...]

  template < std::size_t N_ = N>
  typename std::enable_if<N_==2, T>::type determinant() const
  {
    T det = 0;
    // [...]
    std::cout << "Determinant for 2x2 matrix" << std::endl;
    return det ;
  }

  template < std::size_t N_ = N>
  typename std::enable_if<N_==3, T>::type determinant() const
  {
    T det = 0;
    // [...]
    std::cout << "Determinant for 3x3 matrix" << std::endl;
    return det ;
  }


  private:
   T data[N*N];
};


int main()
{
  Matrix<int, 3> mat1;
  mat1.determinant();
}

But I do not understand why I have to defined this additional template parameter template<std::size_t N_= N> for my method. I would naturally implemented it as follow:

#include <type_traits>
#include <iostream>

template <typename T, std::size_t N>
class Matrix
{
  public:
    Matrix() = default;

    // [...]

  typename std::enable_if<N==2, T>::type determinant() const
  {
    T det = 0;
    // [...]
    std::cout << "Determinant for 2x2 matrix" << std::endl;
    return det ;
  }

  typename std::enable_if<N==3, T>::type determinant() const
  {
    T det = 0;
    // [...]
    std::cout << "Determinant for 3x3 matrix" << std::endl;
    return det ;
  }


  private:
   T data[N*N];
};


int main()
{
  Matrix<int, 3> mat1;
  mat1.determinant();
}

But it does not compile and has the following compilation errors:


<source>:20:42: error: 'typename std::enable_if<(N == 3), T>::type Matrix<T, N>::determinant() const' cannot be overloaded with 'typename std::enable_if<(N == 2), T>::type Matrix<T, N>::determinant() const' [-Wtemplate-body]
   20 |   typename std::enable_if<N==3, T>::type determinant() const
      |                                          ^~~~~~~~~~~
<source>:12:42: note: previous declaration 'typename std::enable_if<(N == 2), T>::type Matrix<T, N>::determinant() const'
   12 |   typename std::enable_if<N==2, T>::type determinant() const
      |                                          ^~~~~~~~~~~
<source>: In instantiation of 'class Matrix<int, 3>':
<source>:36:18:   required from here
   36 |   Matrix<int, 3> mat1;
      |                  ^~~~
[... and more ...]

Why ?

What would be the best approach to implement my class ?

Upvotes: 2

Views: 64

Answers (1)

463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 123114

Those are not partial specializations. Partial specialization is only available for class (and variable) templates (see here). It is rather an attempt to SFINAE on the template paramter N and have only one method succeed, because all the others will have N==? as false. SFINAE however, means Substitution Failure Is Not An Error. It needs substitution to kick in. And substitution requires the method to be a method template.

There is no substitution on typename std::enable_if<N==2, T>::type determinant() const because it is not a template. Its a non-template method of a class template. The template arguments are already substituted when you instantiate the class. To SFINAE on the method the substitution has to take place on the method.

When you write

template < std::size_t N_ = N>
typename std::enable_if<N_==2, T>::type determinant() const

then the compiler will instantiate the method when you call it and part of that is to substitute the template parameter N_. SFINAE kicks in and all is good.

The error message is because all overloads of the method have the same signature. You cannot overload determinant with a second method of the same signature. Only if you apply SFINAE correctly, all but the one that does not result in a substitution failure will be discarded from the overload set.

What would be the best approach to implement my class ?

"best" is subjective. The way you have it already in your second variant is proper SFINAE that works as expected.

As you probably want several methods for the matrices of certain size, a whole different approach would be to implment all methods for a certain size in base classes and then pick the right base depending on N (https://godbolt.org/z/fsMro44qv).

Upvotes: 2

Related Questions