Custom subclasses

Issue #120 resolved
Jannik Schürg created an issue

Hi Klaus,

I did try to subclass a Blaze type in a naive way. My motivation was to use a custom memory allocation, I used the CustomMatrix class. A subclass seemed to be an attractive choice in comparison to managing separate smart pointer objects.

I suppose this is not expected to work by design? Is there a simple way to achieve this without duplicating a lot of code? When searching how this is done in similar libraries I found this document for Eigen.

Current workaround is to overload the dereference operator to get the Blaze type back.

Kind regards

Jannik

Comments (6)

  1. Klaus Iglberger

    Hi Jannik!

    Thanks for creating this proposal. Unfortunately you don't mention what is not working in the description, which leaves me wondering whether you propose a code modification or to add some guidelines similar to the linked webpage to the wiki and tutorial.

    Assuming that you need some guidelines, here is a first attempt to give some help. Below you will find a subclass based on CustomVector, which should work well for most purposes:

    template< typename Type                    // Data type of the matrix
            , bool SO = defaultStorageOrder >  // Storage order
    class MyCustomMatrix
       : public CustomMatrix< Type, unaligned, unpadded, SO >
    {
     public:
       explicit inline MyCustomMatrix( size_t m, size_t n )
          : CustomMatrix<Type,unaligned,unpadded,SO>()
          , array_( new Type[m*n] )
       {
          this->reset( array_.get(), m, n );
       }
    
     private:
       std::unique_ptr<Type[]> array_;
    };
    

    This approach should only be used if you want to add data members. If you want to add functionality, I recommend to add a free function as Blaze favors the free function approach (the reasons will be explained in the talk "Free Your Functions!" at CppCon 2017 and Meeting C++ 2017). Issue #87 and #72 might give you some idea how to proceed in that case.

    I hope this gives you some idea how to get started.

    Best regards,

    Klaus!

  2. Jannik Schürg reporter

    Hi Klaus,

    thank you for this answer 🙂 I did look closer what was the problem in my case. If I use the subclass then some type traits from Blaze are no longer working, which I use in my code. I first thought this would also break things from Blaze, but everything seems to be fine. An example which is similar to my code

    template <typename M>
    std::enable_if<IsDenseMatrix<M>::value && IsRowMajorMatrix<M>::value>::type func(M& m) {
      // ...
      const int rows = m.rows();
      // ...
    }
    

    will not compile because IsDenseMatrix<M>::value will be false for the subclass. Questionable if these traits are part of the API, so I now think I have to blame myself.

    And you are right the proposal is not clear. The proposal was to make subclassing possible (it is), or if it is possible but not straightforward (it is), then create a page like the linked one to document how.

    Kind regards

    Jannik

  3. Klaus Iglberger

    Hi Jannik!

    Thanks for the feedback. I will consider this issue as if the title was "Create wiki page for the customization of types and operations". Also, you are correct that the mentioned type traits stop working as they are implemented too type specific. I will fix this as quickly as possible. For now, please just provide the correct specializations manually. For instance:

    template< typename Type, bool SO >
    struct IsDenseMatrix< MyCustomMatrix<Type,SO> >
       : public BoolConstant<true>
    {};
    
    template< typename Type >
    struct IsRowMajorMatrix< MyCustomMatrix<Type,rowMajor> >
       : public BoolConstant<true>
    {};
    

    Best regards,

    Klaus!

  4. Klaus Iglberger

    Summary

    The tutorial and the wiki have been extended by a page for customizing existing vector and matrix classes. The updated tutorial is immediately available via cloning the Blaze repository. The wiki will be updated with the official release of Blaze 3.2.

    Custom Data Members

    So far the Blaze library does not provide a lot of flexibility to customize the data members of existing vector types and matrix types. However, to some extend it is possible to customize vectors and matrices by inheritance. The following example gives an impression on how to create a simple variation of CustomMatrix, which automatically takes care of acquiring and releasing custom memory.

    template< typename Type                    // Data type of the matrix
            , bool SO = defaultStorageOrder >  // Storage order
    class MyCustomMatrix
       : public CustomMatrix< Type, unaligned, unpadded, SO >
    {
     public:
       explicit inline MyCustomMatrix( size_t m, size_t n )
          : CustomMatrix<Type,unaligned,unpadded,SO>()
          , array_( new Type[m*n] )
       {
          this->reset( array_.get(), m, n );
       }
    
     private:
       std::unique_ptr<Type[]> array_;
    };
    

    Please note that this is a simplified example with the intent to show the general approach. The number of constructors, the memory acquisition, and the kind of memory management can of course be adapted to specific requirements. Also, please note that since none of the Blaze vectors and matrices have virtual destructors polymorphic destruction cannot be used.

    Custom Operations

    There are two approaches to extend Blaze with custom operations. First, the map() functions provide the possibility to execute componentwise custom operations on vectors and matrices. Second, it is possible to add customized free functions.

    The map() Functions

    Via the unary and binary map() functions it is possible to execute componentwise custom operations on vectors and matrices. The unary map() function can be used to apply a custom operation on each single element of a dense vector or matrix or each non-zero element of a sparse vector or matrix. For instance, the following example demonstrates a custom square root computation on a dense matrix:

    blaze::DynamicMatrix<double> A, B;
    
    B = map( A, []( double d ) { return std::sqrt( d ); } );
    

    The binary map() function can be used to apply an operation pairwise to the elements of two dense vectors or two dense matrices. The following example demonstrates the merging of two matrices of double precision values into a matrix of double precision complex numbers:

    blaze::DynamicMatrix<double> real{ { 2.1, -4.2 }, { 1.0,  0.6 } };
    blaze::DynamicMatrix<double> imag{ { 0.3,  1.4 }, { 2.9, -3.4 } };
    
    blaze::DynamicMatrix< complex<double> > cplx;
    
    // Creating the matrix
    //    ( (-2.1,  0.3) (-4.2, -1.4) )
    //    ( ( 1.0,  2.9) ( 0.6, -3.4) )
    cplx = map( real, imag, []( double r, double i ){ return complex( r, i ); } );
    

    These examples demonstrate the most convenient way of defining a unary custom operation by passing a lambda to the map() function. Alternatively, it is possible to pass a custom functor:

    struct Sqrt
    {
       double operator()( double a ) const
       {
          return std::sqrt( a );
       }
    };
    
    B = map( A, Sqrt() );
    

    In order for the functor to work in a call to map() it must define a function call operator, which accepts arguments of the type of the according vector or matrix elements.

    Although the operation is automatically parallelized depending on the size of the vector or matrix, no automatic vectorization is possible. In order to enable vectorization, a load() function can be added to the functor, which handles the vectorized computation. Depending on the data type this function is passed one of the following Blaze SIMD data types:

    • SIMD data types for fundamental data types
      • blaze::SIMDint8: Packed SIMD type for 8-bit signed integral data types
      • blaze::SIMDuint8: Packed SIMD type for 8-bit unsigned integral data types
      • blaze::SIMDint16: Packed SIMD type for 16-bit signed integral data types
      • blaze::SIMDuint16: Packed SIMD type for 16-bit unsigned integral data types
      • blaze::SIMDint32: Packed SIMD type for 32-bit signed integral data types
      • blaze::SIMDuint32: Packed SIMD type for 32-bit unsigned integral data types
      • blaze::SIMDint64: Packed SIMD type for 64-bit signed integral data types
      • blaze::SIMDuint64: Packed SIMD type for 64-bit unsigned integral data types
      • blaze::SIMDfloat: Packed SIMD type for single precision floating point data
      • blaze::SIMDdouble: Packed SIMD type for double precision floating point data
    • SIMD data types for complex data types
      • blaze::SIMDcint8: Packed SIMD type for complex 8-bit signed integral data types
      • blaze::SIMDcuint8: Packed SIMD type for complex 8-bit unsigned integral data types
      • blaze::SIMDcint16: Packed SIMD type for complex 16-bit signed integral data types
      • blaze::SIMDcuint16: Packed SIMD type for complex 16-bit unsigned integral data types
      • blaze::SIMDcint32: Packed SIMD type for complex 32-bit signed integral data types
      • blaze::SIMDcuint32: Packed SIMD type for complex 32-bit unsigned integral data types
      • blaze::SIMDcint64: Packed SIMD type for complex 64-bit signed integral data types
      • blaze::SIMDcuint64: Packed SIMD type for complex 64-bit unsigned integral data types
      • blaze::SIMDcfloat: Packed SIMD type for complex single precision floating point data
      • blaze::SIMDcdouble: Packed SIMD type for complex double precision floating point data

    All SIMD types provide the value data member for a direct access to the underlying intrinsic data element. In the following example, this intrinsic element is passed to the AVX function _mm256_sqrt_pd():

    struct Sqrt
    {
       double operator()( double a ) const
       {
          return std::sqrt( a );
       }
    
       SIMDdouble load( const SIMDdouble& a ) const
       {
          return _mm256_sqrt_pd( a.value );
       }
    };
    

    In this example, whenever vectorization is generally applicable, the load() function is called instead of the function call operator for as long as the number of remaining elements is larger-or-equal to the width of the packed SIMD type. In all other cases (which also includes peel-off and remainder loops) the scalar operation is used.

    Please note that this example has two drawbacks: First, it will only compile in case the intrinsic _mm256_sqrt_pd() function is available (i.e. when AVX is active). Second, the availability of AVX is not taken into account. The first drawback can be alleviated by making the load() function a function template. The second drawback can be dealt with by adding a simdEnabled() function template to the functor:

    struct Sqrt
    {
       double operator()( double a ) const
       {
          return std::sqrt( a );
       }
    
       template< typename T >
       T load( const T& a ) const
       {
          return _mm256_sqrt_pd( a.value );
       }
    
       template< typename T >
       static constexpr bool simdEnabled() {
    #if defined(__AVX__)
          return true;
    #else
          return false;
    #endif
       }
    };
    

    The simdEnabled() function must be a static, constexpr function and must return whether or not vectorization is available for the given data type T. In case the function returns true, the load() function is used for a vectorized evaluation, in case the function returns false, load() is not called.

    Note that this is a simplified example that is only working when used for dense vectors and matrices with double precision floating point elements. The following code shows the complete implementation of the according functor that is used within the Blaze library. The Blaze Sqrt functor is working for all data types that are providing a square root operation:

    namespace blaze {
    
    struct Sqrt
    {
       template< typename T >
       BLAZE_ALWAYS_INLINE auto operator()( const T& a ) const
       {
          return sqrt( a );
       }
    
       template< typename T >
       static constexpr bool simdEnabled() { return HasSIMDSqrt<T>::value; }
    
       template< typename T >
       BLAZE_ALWAYS_INLINE auto load( const T& a ) const
       {
          BLAZE_CONSTRAINT_MUST_BE_SIMD_PACK( T );
          return sqrt( a );
       }
    };
    
    } // namespace blaze
    

    The same approach can be taken for binary custom operations. The following code demonstrates the Min functor of the Blaze library, which is working for all data types that provide a min() operation:

    struct Min
    {
       explicit inline Min()
       {}
    
       template< typename T1, typename T2 >
       BLAZE_ALWAYS_INLINE decltype(auto) operator()( const T1& a, const T2& b ) const
       {
          return min( a, b );
       }
    
       template< typename T1, typename T2 >
       static constexpr bool simdEnabled() { return HasSIMDMin<T1,T2>::value; }
    
       template< typename T1, typename T2 >
       BLAZE_ALWAYS_INLINE decltype(auto) load( const T1& a, const T2& b ) const
       {
          BLAZE_CONSTRAINT_MUST_BE_SIMD_PACK( T1 );
          BLAZE_CONSTRAINT_MUST_BE_SIMD_PACK( T2 );
          return min( a, b );
       }
    };
    

    For more information on the available Blaze SIMD data types and functions, please see the SIMD module in the complete Blaze documentation.

    Free Functions

    In order to extend Blaze with new functionality it is possible to add free functions. Free functions can be used either as wrappers around calls to the map() function or to implement general, non-componentwise operations. The following two examples will demonstrate both ideas.

    The first example shows the setToZero() function, which resets a sparse matrix to zero without affecting the sparsity pattern. It is implemented as a convenience wrapper around the map() function:

    template< typename MT  // Type of the sparse matrix
            , bool SO >    // Storage order
    void setToZero( blaze::SparseMatrix<MT,SO>& mat )
    {
       (~mat) = blaze::map( ~mat, []( int ){ return 0; } );
    }
    

    The blaze::SparseMatrix class template is the base class for all kinds of sparse matrices and provides an abstraction from the actual type MT of the sparse matrix. However, due to the Curiously Recurring Template Pattern (CRTP) it also enables a conversion back to the actual type. This downcast is performed via the tilde operator (i.e. operator~()). The template parameter SO represents the storage order (blaze::rowMajor or blaze::columnMajor) of the matrix.

    The second example shows the countZeros() function, which counts the number of values, which are exactly zero, in a dense, row-major matrix:

    template< typename MT >
    size_t countZeros( blaze::DenseMatrix<MT,rowMajor>& mat )
    {
       const size_t M( (~mat).rows() );
       const size_t N( (~mat).columns() );
       size_t count( 0UL );
    
       for( size_t i=0UL; i<M; ++i ) {
          for( size_t j=0UL; j<N; ++j ) {
             if( blaze::isDefault<strict>( (~mat)(i,j) ) )
                ++count;
          }
       }
    
       return count;
    }
    

    The blaze::DenseMatrix class template is the base class for all kinds of dense matrices. Again, it is possible to perform the conversion to the actual type via the tilde operator.

    The following two listings show the declarations of all vector and matrix base classes, which can be used for custom free functions:

    template< typename VT  // Concrete type of the dense or sparse vector
            , bool TF >    // Transpose flag (blaze::columnVector or blaze::rowVector)
    class Vector;
    
    template< typename VT  // Concrete type of the dense vector
            , bool TF >    // Transpose flag (blaze::columnVector or blaze::rowVector)
    class DenseVector;
    
    template< typename VT  // Concrete type of the sparse vector
            , bool TF >    // Transpose flag (blaze::columnVector or blaze::rowVector)
    class SparseVector;
    
    template< typename MT  // Concrete type of the dense or sparse matrix
            , bool SO >    // Storage order (blaze::rowMajor or blaze::columnMajor)
    class Matrix;
    
    template< typename MT  // Concrete type of the dense matrix
            , bool SO >    // Storage order (blaze::rowMajor or blaze::columnMajor)
    class DenseMatrix;
    
    template< typename MT  // Concrete type of the sparse matrix
            , bool SO >    // Storage order (blaze::rowMajor or blaze::columnMajor)
    class SparseMatrix;
    

    Custom Data Types

    The Blaze library tries hard to make the use of custom data types as convenient, easy and intuitive as possible. However, unfortunately it is not possible to meet the requirements of all possible data types. Thus it might be necessary to provide Blaze with some additional information about the data type. The following sections give an overview of the necessary steps to enable the use of the hypothetical custom data type custom::double_t for vector and matrix operations. For example:

    blaze::DynamicVector<custom::double_t> a, b, c;
    // ... Resizing and initialization
    c = a + b;
    

    The Blaze library assumes that the custom::double_t data type provides operator+() for additions, operator-() for subtractions, operator*() for multiplications and operator/() for divisions. If any of these functions is missing it is necessary to implement the operator to perform the according operation. For this example we assume that the custom data type provides the four following functions instead of operators:

    namespace custom {
    
    double_t add ( const double_t& a, const double_t b );
    double_t sub ( const double_t& a, const double_t b );
    double_t mult( const double_t& a, const double_t b );
    double_t div ( const double_t& a, const double_t b );
    
    } // namespace custom
    

    The following implementations will satisfy the requirements of the Blaze library:

    inline custom::double_t operator+( const custom::double_t& a, const custom::double_t& b )
    {
       return add( a, b );
    }
    
    inline custom::double_t operator-( const custom::double_t& a, const custom::double_t& b )
    {
       return sub( a, b );
    }
    
    inline custom::double_t operator*( const custom::double_t& a, const custom::double_t& b )
    {
       return mult( a, b );
    }
    
    inline custom::double_t operator/( const custom::double_t& a, const custom::double_t& b )
    {
       return div( a, b );
    }
    

    Blaze will use all the information provided with these functions (for instance the return type) to properly handle the operations. In the rare case that the return type cannot be automatically determined from the operator it might be additionally necessary to provide a specialization of the following four Blaze class templates:

    namespace blaze {
    
    template<>
    struct AddTrait<custom::double_t,custom::double_t> {
       typedef custom::double_t  Type;
    };
    
    template<>
    struct SubTrait<custom::double_t,custom::double_t> {
       typedef custom::double_t  Type;
    };
    
    template<>
    struct MultTrait<custom::double_t,custom::double_t> {
       typedef custom::double_t  Type;
    };
    
    template<>
    struct DivTrait<custom::double_t,custom::double_t> {
       typedef custom::double_t  Type;
    };
    
    } // namespace blaze
    

    The same steps are necessary if several custom data types need to be combined (as for instance custom::double_t and custom::float_t). Note that in this case both permutations need to be taken into account:

    custom::double_t operator+( const custom::double_t& a, const custom::float_t& b );
    custom::double_t operator+( const custom::float_t& a, const custom::double_t& b );
    // ...
    

    Please note that only built-in data types apply for vectorization and thus custom data types cannot achieve maximum performance!

  5. Log in to comment