17

I have a Matrix class template as follows:

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }
};

What I want is to define a .setIdentity() function only for instantiations when nrows==ncols is true at compile time. And there will be no definition of .setIdentity() when nrows==ncols is false.

What I am trying is using enable_if idiom, but that will define the function for all cases. Isn't it?

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • 4
    Maybe instead of mutating existing objects conditionally, you just have a static function like `make_identity` that returns a `Matrix`. – GManNickG Aug 25 '16 at 20:28
  • @GManNickG: Favorite solution so far (because it's the simplest!). Please consider adding it as an answer. – Matthieu M. Aug 26 '16 at 00:39
  • Does this answer your question? [C++ templates: conditionally enabled member function](https://stackoverflow.com/questions/26633239/c-templates-conditionally-enabled-member-function) – user202729 Dec 17 '21 at 13:17

5 Answers5

14

You can do it with std::enable_if in the following mode

template <std::size_t r = nrows, std::size_t c = ncols>
typename std::enable_if<r == c>::type setIdentity ()
 { /* do something */ }

A full example

#include <type_traits>


template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
    T data[nrows][ncols];

public:
    T& operator ()(std::size_t i, std::size_t j)
    { return data[i][j]; }

    template <std::size_t r = nrows, std::size_t c = ncols>
    typename std::enable_if<r == c>::type setIdentity ()
     { /* do something */ }
};

int main()
 {
   Matrix<int, 3, 3>  mi3;
   Matrix<int, 3, 2>  mnoi;

   mi3.setIdentity();
   // mnoi.setIdentity(); error

  return 0;
}

--- EDIT ---

As pointed in a comment by Niall (regarding the TemplateRex's answer, but my solution suffer from the same defect) this solution can be circonvented expliciting the number of rows and columns in this way

mi3.setIdentity<4, 4>();

(but this isn't a real problem (IMHO) because mi3 is a square matrix and setIdentity() could work with real dimensions (nrows and ncols)) or even with

mnoi.setIdentity<4, 4>()

(and this is a big problem (IMHO) because mnoi isn't a square matrix).

Obviously there is the solution proposed by Niall (add a static_assert; something like

    template <std::size_t r = nrows, std::size_t c = ncols>
    typename std::enable_if<r == c>::type setIdentity ()
     {
       static_assert(r == nrows && c == ncols, "no square matrix");

       /* do something else */ 
     }

or something similar) but I propose to add the same check in std::enable_if.

I mean

template <std::size_t r = nrows, std::size_t c = ncols>
typename std::enable_if<    (r == c)
                         && (r == nrows)
                         && (c == ncols)>::type setIdentity ()
 { /* do something */ }
max66
  • 65,235
  • 10
  • 71
  • 111
  • should guard against `mi3.setidentity<4,4>()` – TemplateRex Aug 26 '16 at 18:57
  • 1
    @TemplateRex - thanks for reporting the problem; answer modified. – max66 Aug 26 '16 at 19:22
  • Yes, the guard is cleaner ibside the enable_if, error from clang is quite informative, +1 – TemplateRex Aug 27 '16 at 08:39
  • The circumvention problem can be solved by adding a `typename ...` before the template parameters. Template parameter packs will be deduced as empty in normal cases, and the user can't supply the other parameters explicitly anymore. – L. F. Mar 20 '20 at 04:43
  • @L.F. - Wow! Your solution is very elegant. And works. But I don't understand how, exactly, works. I mean... I see that writing `mnoi.setIdentity<3u, 3u>();`, or also `mnoi.setIdentity();`, gives a compilation error. But why you can't explicate arguments after a variadic pack if the arguments are differents? I mean... can you say what part of the standard forbid this? – max66 Mar 20 '20 at 19:25
  • @max66 I didn't really think of that :D I guess there's some rule like "arguments can't be supplied after a pack," but I'll need to dig into the standard further to find out. – L. F. Mar 21 '20 at 02:13
  • @L.F. - Maybe a little less elegant... but writing `template ` avoid the circumvent problem because if you explicit some `std::size_t` they are adsorbed in the unnamed `std::size_t...` pack. Drawback (?): no compilation error. – max66 Mar 21 '20 at 11:47
  • Why do we need `r` and `c` (or something)? e.g. `template ` `typename std::enable_if<(nrows == ncols) && foo == 123>::type setIdentity ()` – c z Jun 21 '23 at 14:12
10

The lazy and needlessly repetitive way

Just add a partial specialization:

template<typename T, std::size_t N>
class Matrix<T, N, N>
{
    T data[N][N];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }

    void setidentity(/*whatever params*/) { std::cout << "yay!"; }
};

Live Example

For general N * M matrices, the general template will be instantiated, whereas only for N * N matrics, this specialization is a better match.

Disadvantage: code repetition of all regular code. Could use a base class, but it's actually easier to do some SFINAE magic (below)

A slightly harder but more economical way

You can also use SFINAE by adding hidden template parameters N and M that default to nrows and ncols to setidentity, and to enable_if on the condition N == M.

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }

    template <std::size_t N = nrows, std::size_t M = ncols, std::enable_if_t<(N == M)>* = nullptr>
    void setidentity(/*whatever params*/) { 
        static_assert(N == nrows && M == ncols, "invalid");
        std::cout << "yay!"; 
    }
};

Or, since the question was tagged C++11, use typename std::enable_if<(N == M)>::type instead.

Live Example

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • 1
    This solution seems to repeat all the existing work the non-special case has already done. Is it really worth the repetition? – GManNickG Aug 25 '16 at 20:28
  • This requires implementing the entire class twice. – Yakk - Adam Nevraumont Aug 25 '16 at 20:29
  • @TemplateRex: Your 2nd code doesn't seem to compile even `m22.setidentity()` line. See my link [link](http://www.tutorialspoint.com/compile_cpp11_online.php?PID=0Bw_CjBb95KQMTEdSZERtMkFVdHc). I need it to be `C++11` compatible, seems you have used GCC 6.1.0 version. –  Aug 25 '16 at 20:53
  • @TemplateRex: Thanks a lot. –  Aug 26 '16 at 09:04
  • 1
    @TemplateRex: actually even with your newer solution for C++11, link [link](http://www.tutorialspoint.com/compile_cpp11_online.php?PID=0Bw_CjBb95KQMb3JvRUtpN0p4ems). See the link, it gives `error: no type named 'type' in 'struct std::enable_if'`. –  Aug 26 '16 at 09:24
  • @smlq thanks for noticing this, I fixed it. The problem was that the template parameters of `setidentity` need to be used in the `enable_if` condition. Now it should work. You can still apply the `typename ::type` for C++11, [like this](http://coliru.stacked-crooked.com/a/21b0db6a5b8b1365) – TemplateRex Aug 26 '16 at 09:51
  • 2
    I might add a `static_assert( N == nrows && M == ncols, "invalid");` to the `setidentity` to avoid silly client behaviour like; `Matrix sq; sq.setidentity<4,4>();` – Niall Aug 26 '16 at 09:56
7

Use a pseudo-CRTP to add modular support for something.

template<class T, std::size_t nrows, std::size_t ncols>
class Matrix;

template<class T, std::size_t size>
struct MatrixDiagonalSupport {
  auto self() { return static_cast<Matrix<T, size, size>*>(this); }
  auto self() const { return static_cast<Matrix<T, size, size> const*>(this); }
  void setIdentity() {
    for (std::size_t i = 0; i < size; ++i) {
      for (std::size_t j = 0; j < i; ++j) {
        (*self())(i,j) = {};
      }
      (*self())(i,i) = 1; // hope T supports this!
      for (std::size_t j = i+1; j < size; ++j) {
        (*self())(i,j) = {};
      }
    }
  }
};
template<class T>
struct empty_t {};
template<bool b, class T>
using maybe= std::conditional_t<b, T, empty_t<T>>;

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix: public maybe<nrows==ncols,MatrixDiagonalSupport<T, nrows>>
{
  // ...

Here we inherit from nothing if we aren't diagonal, and a class implementing set identity if we are diagonal.

Users of Matrix get .setIdentity() from its parent magically if it is right.

static_cast inside self() ends up being a zero-cost abstraction and giving the base class access to the child class.

This is pseudo-CRTP because we don't actually pass the derived class type to the parent, just enough information for the parent to reconstruct it.

This solution makes the method an actual method, and avoids any kind of SFINAE trickery.

Live example

In C++11 replace conditional_t<?> with typename conditional<?>::type:

template<bool b, class T>
using maybe=typename std::conditional<b, T, empty_t<T>>::type;

and everything should compile.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • cute idea, but the `empty_t` needs to be tagged in case there are multiple modules to support – TemplateRex Aug 25 '16 at 20:36
  • @template `maybe` written to make that seamless. – Yakk - Adam Nevraumont Aug 25 '16 at 22:11
  • @Yakk: Would this usage be called a mixin? (seems so to me...) – Matthieu M. Aug 26 '16 at 00:38
  • @matt no idea what pattern is called. Sorry. Even patterns I use all the time I often get the name wrong. – Yakk - Adam Nevraumont Aug 26 '16 at 03:12
  • @Yakk: Thanks a lot. –  Aug 26 '16 at 09:04
  • Wouldn't `setIdentity` complain of undefined reference to `operator()` because it is actually defined way down in the source file? And couldn't this `static_cast` be considered UB? – JoaoBapt Jul 14 '20 at 16:12
  • @JoaoBapt I'm sorry, I don't have a source file in my example, so I'm not sure what you are talking about. As for the static cast, no, you can do a downcast so long as the object types actually match; if you get it wrong, it is UB. – Yakk - Adam Nevraumont Jul 14 '20 at 19:19
  • `(*self())(i,i)` calls `Matrix::operator()`, which is only really declared/defined after `MatrixDiagonalSupport` is defined. I’m afraid this might cause undefined reference errors. – JoaoBapt Jul 14 '20 at 19:44
  • @JoaoBapt No, it won't. The CRTP is a common, well known template strategy in C++. If you have questions about how the CRTP works, google it, and if that doesn't work, [ask a question] using the stack overflow interface. – Yakk - Adam Nevraumont Jul 14 '20 at 20:42
6

A basic, but simple solution not mentioned by any other answer: you can use std::conditional and inheritance.
It follows a minimal, working example:

#include<type_traits>
#include<cstddef>

struct HasSetIdentity {
    void setIdentity() { }
};

struct HasNotSetIdentity {};

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix: public std::conditional<(nrows==ncols), HasSetIdentity, HasNotSetIdentity>::type
{
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }
};

int main() {
    Matrix<int, 2,2> m1;
    m1.setIdentity();
    Matrix<int, 2,3> m2;
    // Method not available
    // m2.setIdentity();
}

You can still move data down the hierarchy if you need them to be shared by all the subobjects.
It mostly depends on the real problem.

skypjack
  • 49,335
  • 19
  • 95
  • 187
1

skypjack and max66 have both presented simple answers to the problem. This is just an alternate way of doing it, using simple inheritance, although it means the use of a child class for square matrices:

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
protected:
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }
};

template<typename T, std::size_t N>
class SqMatrix : public Matrix <T, N, N>
{
public:
    setIdentity()
    {
        //Do whatever
    }
}
Community
  • 1
  • 1
Artezanz
  • 126
  • 6