type_traits question

Discussion in 'General Programming Support' started by camelCase, Apr 6, 2012.

  1. camelCase

    camelCase The Case of the Mysterious Camel.

    How would I make a template class instantiable only with built-in types?
    Like, only integers, chars, bools, enums, floats and pointers.

    I haven't used type_traits at all because I never really got *how* to use it.

    Like, for example, this should fail:
    Code:
    class NonBuiltIn {};
    int main () {
        TemplateClass<NonBuiltIn> this_should_fail; //Compile error
        return 0;
    }
    
  2. s3rius

    s3rius Linux is only free if your time is worthless.

    Not too versed with template metaprogramming myself, but I know the basics (partially :p)

    Code:
    //Normal class template definition, we need a dummy parameter here though, we default it to void so it doesn't bother anyone.
    template<class T, class Dummy=void>
    class Builtin; // The actual class definition is incomplete. If this template overload is chosen, you'll get a "class incomplete" error.
    
    //This is the actual class definition. We change the dummy parameter to enable_if, which only allows this overload
    //if the condition returns true. Our condition is is_fundamental, which tests whether the type is void or arithmetic.
    template<class T>
    class Builtin<T, typename std::enable_if<std::is_fundamental<T>::value >::type> {
    };
    
    
    void someFun(){
         Builtin<std::string> a; //Error
         Builtin<float> b; //Success
    }
    
    
    The technique uses SFINAE (substitution failure is not an error). It'll always pick the most appropriate overload for instantiation. If the template parameter T satisfies is_fundamental<T> then it'll become a suited overload. If not, the only overload left is the one with the dummy parameter. Since this one implements Builtin only incomplete it'll return an error.

    There's a list of all std type traits
    http://msdn.microsoft.com/en-us/library/ff926129.aspx
  3. camelCase

    camelCase The Case of the Mysterious Camel.

    Oo..
    It worked!

    However, if I need some methods in that class..
    I find myself doing this:
    Code:
    //Normal class template definition, we need a dummy parameter here though, we default it to void so it doesn't bother anyone.
    template<class T, class Dummy=void>
    class Builtin; // The actual class definition is incomplete. If this template overload is chosen, you'll get a "class incomplete" error.
    
    //This is the actual class definition. We change the dummy parameter to enable_if, which only allows this overload
    //if the condition returns true. Our condition is is_fundamental, which tests whether the type is void or arithmetic.
    template<class T>
    class Builtin<T, typename std::enable_if<std::is_fundamental<T>::value >::type> {
        BuiltIn method1 (); //return by value
        void method2 ();
        void method3 ();
    };
    
    template<class T>
    Builtin<T, typename std::enable_if<std::is_fundamental<T>::value >::type>
    Builtin<T, typename std::enable_if<std::is_fundamental<T>::value >::type>::method1 () { return stuff; }
    
    template<class T>
    void Builtin<T, typename std::enable_if<std::is_fundamental<T>::value >::type>::method2 () {
        //do stuff
    }
    
    template<class T>
    void Builtin<T, typename std::enable_if<std::is_fundamental<T>::value >::type>::method3 () {
        //do stuff
    }
    
    void someFun(){
         Builtin<std::string> a; //Error
         Builtin<float> b; //Success
    }
    
    And it looks pretty messy to me.
    Is there a way to shorten it? (Besides using a #define macro =x)
  4. s3rius

    s3rius Linux is only free if your time is worthless.

    Yes, there is. Took me a while to figure it out.

    Code:
    
    template<class T, class Dummy=void>
    class Builtin;
    
    template<class T>
    struct allow{
    	typedef typename std::enable_if<std::is_fundamental<T>::value >::type yes;
    };
    
    template<class T>
    class Builtin<T, typename allow<T>::yes> {
        Builtin method1 (); //return by value
        void method2 ();
        void method3 ();
    };
    
    
    template<class T>
    Builtin<T, typename allow<T>::yes >
    Builtin<T, typename allow<T>::yes >::method1 () { return Builtin<T, typename allow<T>::yes>(); }
    
    
    template<class T>
    void Builtin<T, typename allow<T>::yes>::method2 () {
        //do stuff
    }
    
    template<class T>
    void Builtin<T, typename allow<T>::yes>::method3 () {
        //do stuff
    }
    
    void someFun(){
         //Builtin<std::string> a; //Error
         Builtin<float> b; //Success
    }
    
    
    I used the enable_if trick to make a new typedef-in-struct. I think there is something like templated typedefs in C++11 that would make the code shorter, but that's as good as I can do.

    If you're using several of these templated classes it might be a good idea to rename the struct to make them resemble type_traits more closely:

    Code:
    template<class T>
    struct enable_if_fundamental{
    	typedef typename std::enable_if<std::is_fundamental<T>::value >::type type;
    };
    ...
    void Builtin<T, typename enable_if_fundamental<T>::type>::method3 () {
    
    This way you can actually re-use them.
  5. camelCase

    camelCase The Case of the Mysterious Camel.

    Wow, that's neat.
    Thanks for the help.

    I've been learning about matrices and decided to code a C++ class for it (got lazy to do the computations in my head).
    I figured I'd learn template metaprogramming while I was at it.

    Anyway, I've got another problem, it goes something like:
    Code:
    template<typename T, unsigned Rows, unsigned Columns>
    class Matrix {
        T matrix[Rows][Columns];
        public:
        template <typename T, unsigned Rows, unsigned Columns>
        typename std::enable_if<Rows==2 && Columns==2, T>::type
        determinant () const {
            T a = matrix[0][0],
              b = matrix[0][1],
              c = matrix[1][0],
              d = matrix[1][1];
            return a*d - b*c;
        }
        
        typename std::enable_if<Rows==2 && Columns==2, Matrix>::type
        adjugate () const {
            Matrix result(*this);
            std::swap(result[0][0], result[1][1]); //Swap a, d
            result[0][1] *= -1; //-b
            result[1][0] *= -1; //-c
            return result;
        }
                            
        typename std::enable_if<Rows==2 && Columns==2, Matrix>::type
        inverse () const {
            return adjugate() * (1/determinant());
        }
    };
    
    Compile fails if I try to instantiate Matrix<float, 3, 3>, Matrix<float, 1, 2> and any Matrix class that isn't Matrix<T, 2U, 2U>.

    I can't remember where I saw it from, but I remember seeing something like this:
    Code:
    template<stuff>
    class TemplateClass {
        template<other stuff>
        //some method using both "stuff" and "other stuff" to work
    };
    
    And I did use that for some other method specializations (don't know what it's called).

    But for the determinant(), adjugate() and inverse() methods, their specializations don't rely on any "other stuff" template parameters.
    Uhh..
    Not sure if I'm making myself clear =x

    Guess I'm just wondering why this will SFINAE without error:
    Code:
    template<stuff>
    class TemplateClass {
        template<other stuff>
        typename std::enable_if<*some failed condition*>::type //enable_if fails, so this isn't enabled.
        method () {}
    };
    
    While this won't SFINAE without error:
    Code:
    template<stuff>
    class TemplateClass {
        //Compile error because of failure =/
        typename std::enable_if<*some failed condition*>::type //enable_if fails, so this isn't enabled.
        method () {}
    };
    
    And how to get around that error.

    Edit:
    I figured I'd just post what I have so far in case it helps:
    Code:
    #ifndef JAHS_MATRIX_H
    #define JAHS_MATRIX_H
        #include <utility>
        #include <string.h>
        #include <type_traits>
        namespace jahs {
            namespace math {
                template <typename T, unsigned Rows, unsigned Columns>
                class Matrix {
                    public:
                        Matrix ();
                        Matrix (const Matrix &other);
                        explicit Matrix (const T (&arr)[Rows][Columns]);
                        
                        T* operator[] (unsigned row);
                        const T* operator[] (unsigned row) const;
                        Matrix operator+ (const Matrix &other) const;
                        Matrix operator- (const Matrix &other) const;
    
                        #pragma region Matrix<T, Rows, OtherColumns> operator* (const Matrix<T, Columns, OtherColumns> &other) const;
                        template <unsigned OtherColumns>
                        Matrix<T, Rows, OtherColumns> operator* (const Matrix<T, Columns, OtherColumns> &other) const {
                            Matrix<T, Rows, OtherColumns> ret_val;
                            for (unsigned row=0; row<Rows; row++) {
                                for (unsigned col=0; col<OtherColumns; col++) {
                                    T sum = static_cast<T>(0);
                                    for (unsigned n=0; n<Columns; n++) {
                                        sum += matrix[row][n] * other[n][col];
                                    }
                                    ret_val[row][col] = sum;
                                }
                            }
                            return ret_val;
                        }
                        #pragma endregion
                        #pragma region Matrix operator* (U scalar) const;
                        template <typename U>
                        typename std::enable_if<std::is_scalar<U>::value, Matrix>::type
                        operator* (U scalar) const {
                            Matrix result;
                            for (unsigned row=0; row<Rows; row++) {
                                for (unsigned col=0; col<Columns; col++) {
                                    result[row][col] = matrix[row][col]*scalar;
                                }
                            }
                            return result;
                        }
                        #pragma endregion
                        #pragma region static Matrix identity ();
                        
                        static typename std::enable_if<Rows==Columns, Matrix>::type
                        identity () {
                            static Matrix result;
                            if (result[0][0] != static_cast<T>(1)) {
                                for (unsigned i=0; i<Rows; i++) {
                                    result[i][i] = static_cast<T>(1);
                                }
                            }
                            return result;
                        }
                        #pragma endregion
                        #pragma region Matrix operations for two-by-two matrices
                            
                            typename std::enable_if<Rows==2 && Columns==2, T>::type
                            determinant () const {
                                T a = matrix[0][0],
                                  b = matrix[0][1],
                                  c = matrix[1][0],
                                  d = matrix[1][1];
                                return a*d - b*c;
                            }
    
                            typename std::enable_if<Rows==2 && Columns==2, Matrix>::type
                            adjugate () const {
                                Matrix result(*this);
                                std::swap(result[0][0], result[1][1]); //Swap a, d
                                result[0][1] *= -1; //-b
                                result[1][0] *= -1; //-c
                                return result;
                            }
                            
                            typename std::enable_if<Rows==2 && Columns==2, Matrix>::type
                            inverse () const {
                                return adjugate() * (1/determinant());
                            }
                        #pragma endregion
                    private:
                        T matrix[Rows][Columns];
                };
    
                template <typename T, unsigned Rows, unsigned Columns>
                Matrix<T, Rows, Columns>::Matrix () {}
    
                template <typename T, unsigned Rows, unsigned Columns>
                Matrix<T, Rows, Columns>::Matrix (const Matrix &other) {
                    std::memcpy(&matrix[0], &other.matrix[0], sizeof(T) * Rows * Columns);
                }
    
                template <typename T, unsigned Rows, unsigned Columns>
                Matrix<T, Rows, Columns>::Matrix (const T (&arr)[Rows][Columns]) {
                    std::memcpy(&matrix[0], &arr[0], sizeof(T) * Rows * Columns);
                }
    
                template <typename T, unsigned Rows, unsigned Columns>
                T* Matrix<T, Rows, Columns>::operator[] (unsigned row) {
                    return matrix[row];
                }
    
                template <typename T, unsigned Rows, unsigned Columns>
                const T* Matrix<T, Rows, Columns>::operator[] (unsigned row) const {
                    return matrix[row];
                }
    
                template <typename T, unsigned Rows, unsigned Columns>
                Matrix<T, Rows, Columns> Matrix<T, Rows, Columns>::operator+ (const Matrix &other) const {
                    Matrix ret_val;
                    for (unsigned row=0; row<Rows; row++) {
                        for (unsigned col=0; col<Columns; col++) {
                            ret_val[row][col] = matrix[row][col] + other[row][col];
                        }
                    }
                    return ret_val;
                }
    
                template <typename T, unsigned Rows, unsigned Columns>
                Matrix<T, Rows, Columns> Matrix<T, Rows, Columns>::operator- (const Matrix &other) const {
                    Matrix ret_val;
                    for (unsigned row=0; row<Rows; row++) {
                        for (unsigned col=0; col<Columns; col++) {
                            ret_val[row][col] = matrix[row][col] - other[row][col];
                        }
                    }
                    return ret_val;
                }
            }
        }
    #endif
    
    The problem is in the regions "static Matrix identity()" and "Matrix operations for two-by-two matrices".
    All the methods there won't compile with this:
    The errors all come from Rows and Columns not being "2"
    Code:
    #include "Matrix.h"
    #include <iostream>
    using namespace std;
    using namespace jahs::math;
    
    int main () {
        float arr[3][3] = {
            {1, 0, 1},
            {0, 2, 1},
            {1, 1, 1}
        };
        float arr2[3][3] = {
            {-1,-1, 2},
            {-1, 0, 1},
            { 2, 1,-2}
        };
        Matrix<float, 2, 3> t;
        Matrix<float, 3, 3> m(arr);
        Matrix<float, 3, 3> m2(arr2);
        Matrix<float, 3, 3> identity = m2*m;
        Matrix<float, 3, 3> m3 = m*identity;
        Matrix<float, 3, 3> m4 = m2*identity*2.f;
        Matrix<float, 4, 4> m5 = Matrix<float, 4, 4>::identity();
    
        float a_2D_1[2][2] = {
            {1, 2},
            {3, 4}
        };
        Matrix<float, 2, 2> m_2D_1(a_2D_1);
        Matrix<float, 2, 2> m_2D_2 = m_2D_1.inverse();
        Matrix<float, 2, 2> m_2D_3 = m_2D_1 * m_2D_2;
    
        for (unsigned r=0; r<2; r++) {
            for (unsigned c=0; c<2; c++) {
                cout<< m_2D_3[r][c] << " ";
            }
            cout<< endl;
        }
        
        return 0;
    }
    
  6. s3rius

    s3rius Linux is only free if your time is worthless.

    That's because templates are a bitch ^^
    Template name lookup is a two-phase stage.

    I think that all of what I wrote here is sound, however I can't guarantee.

    I've killed most of the matrix:

    Code:
    
    template <typename T, unsigned Rows, unsigned Columns>
    class Matrix {
    	public:
    
    		typename std::enable_if<Rows==Columns, Matrix>::type
    		identity () {
                            
    		}
    
    };
    
    Let's take a look at this, since it's one of the problematic functions. Even though Matrix (the one used in the enable_if) is a dependent type, for the deduction of identity() it's treated as a non-dependent type, because nothing in identity() specifies that Matrix participates in name deduction.
    Which means that identity() can and will be resolved at the point of definition:

    Code:
    void main(){
        Matrix<float, 3,3> m; //Here, code for [B]identity()[/B] must already be deductable for the compiler.
    }
    
    What happens if you now define Matrix<float, 3,4> (which is supposed to not generate identity()?
    Following code will be generated:

    Code:
    typename std::enable_if<3==4, Matrix>::type
    identity () {
    }
    
    What does enable_if<..> evaluate to?
    Let's look at how enable_if should be implemented:
    Code:
    template<bool B, class T = void>
    struct enable_if {};
     
    template<class T>
    struct enable_if<true, T> { typedef T type; };
    
    (from http://en.cppreference.com/w/cpp/types/enable_if)

    So, enable_if is a struct which holds a typedef for T, called type. But only if the condition evaluated to true. If false, it'll not hold anything. So in our case there is no typedef type.

    Code:
    typename std::enable_if<3==4, Matrix>::type //<- for 3==4, type is non-existent
    identity () {
    }
    
    So that's a simple syntax error right here. Why doesn't SFINAE simply remove the invalid template deduction? It should. However, since there is no dependent name used for identity(), SFINAE won't come into action. So, we need to tell compiler that Matrix is has to be treated as a dependent name.

    Code:
    template <typename U>
    typename std::enable_if<3==4, U>::type
    identity () {
    }
    
    By adding a template parameter U we can alleviate the problem. However, calling identity isn't as nice anymore, because we need to specify U:
    Code:
    void main(){
        Matrix<float, 3,3> m;
        m.identity< Matrix<float,3,3> >();
    }
    
    And from here on I'm a bit lost too.
    In C++11 we can specify default values for class template function parameters:
    Code:
    template <typename U = Matrix> //Voila
    typename std::enable_if<3==4, U>::type
    identity () {
    }
    
    Which would get rid of all our problems.
    VS2010, however (which I think you're using too, iirc), doesn't follow the C++11 standard yet. That stuff is only coming somewhen after release of VS2011.

    Soo.... other solutions.. only one comes to mind:
    We need to specify U somehow. We can either explicitely name it (like in the example above) or let argument deduction take over:
    Code:
    template <typename U>
    typename std::enable_if<3==4, U>::type
    identity (U& u) {
    }
    ....
    Matrix<float,3,3> m;
    auto id = m.identity(m); //At least no more <>
    
    And at that point it might seem more useful to make identity() a seperate helper function instead of a class member function.

    I'm pretty sure there must be some semi-magic template hackery to get the job done, I just don't know of them. I'm not very well versed in templates and everything that goes along with it.

    I've looked around on some sites, but I couldn't find much about cases that resemble this specific case (since we don't want member function specialization, but rather prevention).
  7. camelCase

    camelCase The Case of the Mysterious Camel.

    Wooo weeeeeee, as always, you give great explanations, s3rius!
    And that is a nice site >.>

    "In C++11 we can specify default values for class template function parameters:"
    Yeah, I tried that and it failed =/
    I didn't know it was part of the C++11 standard (I was just aimlessly shifting code, praying for a miracle).

    And, yeap, I'm using VS2010.

    I've been trying to find a solution on my own and the closest I got (to me, at least) was something like:
    Code:
    class DefaultMatrixType {}; //Every other matrix
    class R2C2 : public DefaultMatrixType {}; //2.2 matrix
    class R3C3 : public DefaultMatrixType {}; //3.3 matrix
    
    template <unsigned Rows, unsigned Cols, class Dummy=DefaultMatrixType>
    class Operations {
        public:
            /*Constructor that takes reference to 2D array*/
        protected:
            /*const reference to 2D array*/
    };
    
    template <unsigned Rows, unsigned Cols, class Dummy>
    class Matrix {
        public:
            /*All constructors initialize "operations" by passing its internal 2D array*/
            const Operations<Rows, Cols> operations;
    };
    
    template <>
    class Operations<2, 2, R2C2> : public Operations<2, 2> {
        public:
            /*inverse, adjugate, identity, isSingular, determinant*/
            /*All calculated using the 2D array reference in its base class*/
            Matrix<2, 2> inverse () {
                Matrix<2, 2> result;
                //stuff..
                return result;
            }
    }
    
    So, I was thinking I could do this:
    Matrix<2, 2> A(some_2D_array);
    A = A.operations.identity();

    However, this doesn't work because the explicit specialization for class Operations was defined *after* the definition of class Matrix =/
    So, class Matrix uses the first definition of class Operations (that has only a constructor and no methods).

    =(

Share This Page