Search code examples
c++c++11matrixsfinaedecltype

Checking type with template C++


I'm studying the implementation of the Matrix class (explained in Stroustrup's book TC++PL 4th ed.), but I can't really understand some passages.

I found this code:

file traits.h -> https://github.com/statslabs/matrix/blob/master/include/slab/matrix/traits.h

file matrix.h -> https://github.com/statslabs/matrix/blob/master/include/slab/matrix/matrix.h

In matrix.h there is a function (as many other) with Enable_if:

template<typename T, std::size_t N>
template<typename M, typename F>
Enable_if<Matrix_type<M>(), Matrix<T, N> &> Matrix<T, N>::apply(const M &m, F f) {
    /// Some code...
}

and I think Enable_if says: If (M is a Matrix), declare the apply's return type as Matrix<T, N>&.

Then, I want to know how does Matrix_type<M>() works, so I go to traits.h, and I read:

struct substitution_failure {};

template <typename T>
struct substitution_succeeded : std::true_type {};

template <>
struct substitution_succeeded<substitution_failure> : std::false_type {};

template <typename M>
struct get_matrix_type_result {
  template <typename T, size_t N, typename = Enable_if<(N >= 1)>>
  static bool check(const Matrix<T, N> &m);

  template <typename T, size_t N, typename = Enable_if<(N >= 1)>>
  static bool check(const MatrixRef<T, N> &m);

  static substitution_failure check(...);

  using type = decltype(check(std::declval<M>()));
};

template <typename T>
struct has_matrix_type
    : substitution_succeeded<typename get_matrix_type_result<T>::type> {};

template <typename M>
constexpr bool Has_matrix_type() {
  return has_matrix_type<M>::value;
}

template <typename M>
using Matrix_type_result = typename get_matrix_type_result<M>::type;

template <typename M>
constexpr bool Matrix_type() {
  return Has_matrix_type<M>();
}

The first 3 structs describe success and failure cases, the template<> is the specialization of substitution_succeeded that says: if the type of substitution_succeeded is substitution_failure, "return" false else "return" true. I hope that what I'm saying is correct.

Now, get_matrix_type_result is completely obscure. I can't understand why it use a variadic function (check(...)), what are declval and decltype doing in this code and how it is possible that check can return a bool or a substitution_failure. Why not just bool?

Thanks.


Solution

  • Now, get_matrix_type_result is completely obscure. I can't understand why it use a variadic function (check(...)), what are declval and decltype doing in this code and how it is possible that check can return a bool or a "substitution_failure". Why not just bool?

    An important point is that Enable_if (std::enable_if starting from C++11) is designed to enable or not enable something (a template function, a template class specialization, a template method, a template variable specialization).

    On the ground is a C++ principle named SFINAE (Substitution Failure Is Not An Error) that say that, in some places, if a substitution do not take place, it's only a soft error, non an hard error, and the compilation can continue.

    In your case, you have that type is defined as follows

    using type = decltype(check(std::declval<M>()));
    

    where decltype() "return" the type of the argument; in this case, the type returned from a call to check() with an hypothetical object of type M (the template parameter of the class).

    You could write

    using type = decltype(check(M{}));
    

    to pass an object of type M. But this works only with types that are default constructible. The question is: how to use, in a decltype() argument (that has the exclusive function to deduce types, not execute instructions) an object of a generic type if we don't know how construct an object of that type?

    The solution is a function only declared (not defined) as follows

    template<class T>
    typename std::add_rvalue_reference<T>::type declval() noexcept;
    

    This is a trick to have an object of type T (or better: T &) also when do you don't know how to construct it.

    Returning to check, you have three version of it (only declared: are used inside a decltype(); we are only interested in the returned type so there is no need to execute them, so there is no need to define them):

    1) the first one accept a Matrix<T, N> but only (Enable_if) if N >= 1

    template <typename T, size_t N, typename = Enable_if<(N >= 1)>>
    static bool check(const Matrix<T, N> &m);
    

    If you call check() with a Matrix<T, 0>, the Enable_if return nothing so you have a substitution failure (defining the default for a template parameter) so this version of check() isn't enabled

    2) the second one accept a MatrixRef<T, N> but only (Enable_if) if N >= 1

    template <typename T, size_t N, typename = Enable_if<(N >= 1)>>
    static bool check(const MatrixRef<T, N> &m);
    

    Again: if you call check() with a MatrixRef<T, 0>, the Enable_if return nothing so you have a substitution failure (defining the default for a template parameter) so this version of check() isn't enabled

    3) the third one accept everything and is ever enabled

    static substitution_failure check(...);
    

    Conclusion:

    1) if M is a Matrix<T, N> (or an object convertible to a Matrix<T, N>), for some T and some N with N >= 1, the compiler can choose between version (1) and version (3) of check() and choose version (1) because more specialized, that return bool, so type become bool

    2) if M is a MatrixRef<T, N> (or an object convertible to a MatrixRef<T, N>), for some T and some N with N >= 1, the compiler can choose between version (2) and version (3) of check() and choose version (2) because more specialized, that return bool, so type become bool

    3) if M isn't convertible to a Matrix<T, N> or a MatrixRef<T, N>, with N >= 1, the compiler can choose only version (3), that return a substitution_failure, so type become substitution_failure.

    Off Topic: the code you show us seems to me a little overcomplicated.

    By example, if you rewrite get_matrix_type_result as follows

    template <typename M>
    struct get_matrix_type_result {
      template <typename T, size_t N, typename = Enable_if<(N >= 1)>>
      static std::true_type check(const Matrix<T, N> &m);
    
      template <typename T, size_t N, typename = Enable_if<(N >= 1)>>
      static std::true_type check(const MatrixRef<T, N> &m);
    
      static std::false_type check(...);
    
      using type = decltype(check(std::declval<M>()));
    };
    

    you have that type is the type that you needs in has_matrix_type that can be defined as follows

    template <typename T>
    struct has_matrix_type
        : public get_matrix_type_result<T>::type
     { };
    

    avoiding at all substitution_failure and substitution_succeded.

    But, maybe, the code is written this way for other needs.