Search code examples
c++eigeneigen3

Write arbitrary Eigen object to row-major plain storage


I am writing a module to write data to a file which uses by convention only row-major storage. I would like my function to be able to allow both column-major and row-major Eigen objects as input.

Currently I first use Eigen to copy a column-major object to a row-major object, before I write. My code works well for most cases, but for Eigen::VectorXi compiling fails with an assertion that I don't understand. How do I solve this? Can I avoid creating many cases?

The code (writing is mimicked by outputting a std::vector):

#include <vector>
#include <iostream>
#include <Eigen/Eigen>

template <class T, int Rows, int Cols, int Options, int MaxRows, int MaxCols>
std::vector<T> write(const Eigen::Matrix<T,Rows,Cols,Options,MaxRows,MaxCols>& matrix)
{
    std::vector<T> data(static_cast<size_t>(matrix.size()));

    if (matrix.IsRowMajor) {
        std::copy(matrix.data(), matrix.data()+matrix.size(), data.begin());
        return data;
    } else {
        Eigen::Matrix<T, Rows, Cols, Eigen::RowMajor, MaxRows, MaxCols> tmp = matrix;
        return write(tmp);
    }
}

int main()
{
    Eigen::VectorXi matrix = Eigen::VectorXi::LinSpaced(10, 0, 9);

    std::vector<int> output = write(matrix);
}

The compilation error:

In file included from test.cpp:3:
In file included from /usr/local/Cellar/eigen/3.3.7/include/eigen3/Eigen/Eigen:1:
In file included from /usr/local/Cellar/eigen/3.3.7/include/eigen3/Eigen/Dense:1:
In file included from /usr/local/Cellar/eigen/3.3.7/include/eigen3/Eigen/Core:457:
/usr/local/Cellar/eigen/3.3.7/include/eigen3/Eigen/src/Core/PlainObjectBase.h:903:7: error: static_assert failed "INVALID_MATRIX_TEMPLATE_PARAMETERS"
      EIGEN_STATIC_ASSERT((EIGEN_IMPLIES(MaxRowsAtCompileTime==1 && MaxColsAtCompileTime!=1, (Options&RowMajor)==RowMajor)
      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/local/Cellar/eigen/3.3.7/include/eigen3/Eigen/src/Core/util/StaticAssert.h:33:40: note: expanded from macro 'EIGEN_STATIC_ASSERT'
    #define EIGEN_STATIC_ASSERT(X,MSG) static_assert(X,#MSG);
                                       ^             ~
/usr/local/Cellar/eigen/3.3.7/include/eigen3/Eigen/src/Core/PlainObjectBase.h:535:7: note: in instantiation of member function 'Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 1, -1, 1>
      >::_check_template_params' requested here
      _check_template_params();
      ^
/usr/local/Cellar/eigen/3.3.7/include/eigen3/Eigen/src/Core/Matrix.h:377:9: note: in instantiation of function template specialization 'Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 1, -1, 1>
      >::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >' requested here
      : Base(other.derived())
        ^
test.cpp:14:79: note: in instantiation of function template specialization 'Eigen::Matrix<int, -1, 1, 1, -1, 1>::Matrix<Eigen::Matrix<int, -1, 1, 0, -1, 1> >' requested here
        Eigen::Matrix<T, Rows, Cols, Eigen::RowMajor, MaxRows, MaxCols> tmp = matrix;
                                                                              ^
test.cpp:23:31: note: in instantiation of function template specialization 'write<int, -1, 1, 0, -1, 1>' requested here
    std::vector<int> output = write(matrix);
                              ^
1 error generated.

Solution

  • Understanding the static assertion

    Unfortunately the assertion is really not self-explanatory and the only thing you can get from it is the hint, that something is wrong with your template parameters. If we look into Eigen's source code we find the following beginning on line 903:

    EIGEN_STATIC_ASSERT((EIGEN_IMPLIES(MaxRowsAtCompileTime==1 && MaxColsAtCompileTime!=1, (Options&RowMajor)==RowMajor)
                            && EIGEN_IMPLIES(MaxColsAtCompileTime==1 && MaxRowsAtCompileTime!=1, (Options&RowMajor)==0)
                            && ((RowsAtCompileTime == Dynamic) || (RowsAtCompileTime >= 0))
                            && ((ColsAtCompileTime == Dynamic) || (ColsAtCompileTime >= 0))
                            && ((MaxRowsAtCompileTime == Dynamic) || (MaxRowsAtCompileTime >= 0))
                            && ((MaxColsAtCompileTime == Dynamic) || (MaxColsAtCompileTime >= 0))
                            && (MaxRowsAtCompileTime == RowsAtCompileTime || RowsAtCompileTime==Dynamic)
                            && (MaxColsAtCompileTime == ColsAtCompileTime || ColsAtCompileTime==Dynamic)
                            && (Options & (DontAlign|RowMajor)) == Options),
            INVALID_MATRIX_TEMPLATE_PARAMETERS)
    

    Even though the compiler indicates that

    EIGEN_IMPLIES(MaxRowsAtCompileTime==1 && MaxColsAtCompileTime!=1, (Options&RowMajor)==RowMajor)
    

    causes the error, the following line really does:

    EIGEN_IMPLIES(MaxColsAtCompileTime==1 && MaxRowsAtCompileTime!=1, (Options&RowMajor)==0)
    

    Understanding what triggers the assertion

    You provide Eigen::VectorXi as an input for write. Eigen::VectorXi is really just a typedef for

    Eigen::Matrix<int, Eigen::Dynamic, 1, Eigen::ColMajor, Eigen::Dynamic, 1>
    

    Therefore the line

    Eigen::Matrix<T, Rows, Cols, Eigen::RowMajor, MaxRows, MaxCols> tmp = matrix;
    

    in write expands to

    Eigen::Matrix<int, Eigen::Dynamic, 1, Eigen::RowMajor, Eigen::Dynamic, 1> tmp = matrix;
    

    which triggers the assertion, since a matrix with MaxColsAtCompileTime==1 and MaxRowsAtCompileTime!=1 must not be RowMajor.

    Solve your problem

    The problem now is that even though you can check if your input matrix is a vector, row-major or column-major, you cannot declare

    Eigen::Matrix<T, Rows, Cols, Eigen::RowMajor, MaxRows, MaxCols>
    

    if it is no legal to do so at compile-time (and it isn't due to the static assertion).

    You have the following options to make your code work:

    1. if constexpr (C++17)

    C++17 offers a way for detecting at compile-time if a certain conditional branch will be taken or not. The downside of this approach (beside the requirement for a C++17 compiler) is that you can only test for constant expressions.

    In the concrete example this looks like this:

    template <class T, int Rows, int Cols, int Options, int MaxRows, int MaxCols>
    std::vector<T> write(const Eigen::Matrix<T, Rows, Cols, Options, MaxRows, MaxCols>& matrix)
    {
      typedef Eigen::Matrix<T, Rows, Cols, Options, MaxRows, MaxCols> MatrixType;
      std::vector<T> data(static_cast<size_t>(matrix.size()));
    
      if constexpr (MatrixType::MaxRowsAtCompileTime == 1 || 
                    MatrixType::MaxColsAtCompileTime ==1 ||
                   (MatrixType::Options&Eigen::RowMajor) == Eigen::RowMajor) {
        std::copy(matrix.data(), matrix.data() + matrix.size(), data.begin());
        return data;
      } else {
        Eigen::Matrix<T, Rows, Cols, Eigen::RowMajor, MaxRows, MaxCols> tmp = matrix;
        return write(tmp);
      }
    }
    

    2. SFINAE

    You can dispatch the call to write at compile-time using SFINAE by using std::enable_if. The following example uses a slightly modified version of your original code but everything should be clear from context:

    // matrix is either a vector or in row-major
    template <typename Derived>
    std::vector<typename Derived::Scalar> write(const Eigen::MatrixBase<Derived>& matrix,
      typename std::enable_if<Derived::MaxRowsAtCompileTime == 1 ||
                              Derived::MaxColsAtCompileTime == 1 ||
                              (Derived::Options & Eigen::RowMajor) == Eigen::RowMajor,
                              Derived>::type* = 0)
    {
      std::vector<typename Derived::Scalar> data(
          static_cast<size_t>(matrix.size()));
      std::copy(matrix.derived().data(), matrix.derived().data() + matrix.size(),
                data.begin());
      return data;
    }
    
    // matrix is neither a vector nor in row-major
    template <typename Derived>
    std::vector<typename Derived::Scalar> write(const Eigen::MatrixBase<Derived>& matrix,
      typename std::enable_if<Derived::MaxRowsAtCompileTime != 1 &&
                              Derived::MaxColsAtCompileTime != 1 &&
                              (Derived::Options & Eigen::RowMajor) == 0,
                              Derived>::type* = 0)
    {
      Eigen::Matrix<typename Derived::Scalar, Derived::RowsAtCompileTime,
                    Derived::ColsAtCompileTime, Eigen::RowMajor,
                    Derived::MaxRowsAtCompileTime, Derived::MaxColsAtCompileTime> tmp = matrix;
      return write(tmp);
    }
    

    This works using a C++11 compiler.

    Other options would be to specialise the template but it will get even more lengthy than the SFINAE approach.

    Some test cases:

    Eigen::Matrix<int, 3, 3, Eigen::RowMajor> m;
    m << 1, 2, 3, 
         1, 2, 3,
         1, 2, 3;
    
    std::vector<int> output = write(m);
    
    for (const auto& element : output) {
      std::cout << element << " ";
    }
    

    Output: 1 2 3 1 2 3 1 2 3

    Eigen::Matrix<int, 3, 3, Eigen::ColMajor> m;
    m << 1, 2, 3, 
         1, 2, 3,
         1, 2, 3;
    
    std::vector<int> output = write(m);
    
    for (const auto& element : output) {
      std::cout << element << " ";
    }
    

    Output: 1 2 3 1 2 3 1 2 3

    Eigen::VectorXi m = Eigen::VectorXi::LinSpaced(10, 0, 9);
    
    std::vector<int> output = write(m);
    
    for (const auto& element : output) {
      std::cout << element << " ";
    }
    

    Output: 0 1 2 3 4 5 6 7 8 9

    Eigen::RowVectorXi m = Eigen::RowVectorXi::LinSpaced(10, 0, 9);
    
    std::vector<int> output = write(m);
    
    for (const auto& element : output) {
      std::cout << element << " ";
    }
    

    Output: 0 1 2 3 4 5 6 7 8 9