The use of integers isn't arbitrary, they are inherent to an efficient implementation through their fast and direct mapping to and from memory addresses. Any implementation that doesn't use integer indices will naturally degrade performance. The simplest such implementation would be based on an associative array such as std::map<RowKey, std::map<ColumnKey, double>>
.
It seems like you want to provide some sort of meaning to individual rows / columns. If you want to preserve reasonable performance, you should stick with the integer-based sparse matrix format but of course you can map integers from and to arbitrary keys.
template<class T>
struct IndexFunction
{
std::vector<T> index_to_key;
std::unordered_map<T, int> key_to_index;
IndexFunction() = default;
template<class Iterator>
IndexFunction(Iterator first, Iterator last)
: index_to_key(first, last)
{
int index = 0;
for(const T& key: index_to_key)
key_to_index[key] = index++;
}
bool operator==(const IndexFunction& o) const noexcept
{ return this == &o || index_to_key == o.index_to_key; }
bool operator!=(const IndexFunction& o) const noexcept
{ return this != &o && index_to_key != o.index_to_key; }
};
We can then attach this to our matrix type.
template<class Scalar, class RowKey, class ColKey, int Options=0>
struct Matrix
{
using ScalarType = Scalar;
using MatrixType = Eigen::SparseMatrix<Scalar, Options>;
using RowFunction = IndexFunction<RowKey>;
using ColFunction = IndexFunction<ColKey>;
MatrixType matrix;
std::shared_ptr<const RowFunction> row_function;
std::shared_ptr<const ColFunction> col_function;
template<class OtherColKey, int OtherOptions>
friend Matrix<Scalar, RowKey, OtherColKey, Options> operator*(
const Matrix& left,
const Matrix<Scalar, ColKey, OtherColKey, OtherOptions>& right)
{
if(*left.col_function != *right.row_function)
throw std::runtime_error("Incompatible dimensions");
Matrix<Scalar, RowKey, OtherColKey, Options> rtrn;
rtrn.matrix = left.matrix * right.matrix;
rtrn.row_function = left.row_function;
rtrn.col_function = right.col_function;
return rtrn;
}
};
Use it like this:
int main()
{
enum class IndexA { a, b, c, d };
static const IndexA akeys[] = { IndexA::a, IndexA::b, IndexA::c, IndexA::d };
static const double IndexB[] = { 0., 1., 2., 3. };
const int IndexC[] = { 4, 5, 6, 7 };
using MatrixAB = Matrix<double, IndexA, double>;
using MatrixBC = Matrix<double, double, int, Eigen::RowMajor>;
using MatrixAC = Matrix<double, IndexA, int>;
MatrixAB ab;
ab.matrix = Eigen::Matrix4d::Random().sparseView();
ab.row_function = std::make_shared<IndexFunction<IndexA>>(
std::begin(akeys), std::end(akeys));
ab.col_function = std::make_shared<IndexFunction<double>>(
std::begin(IndexB), std::end(IndexB));
MatrixBC bc;
bc.matrix = Eigen::Matrix4d::Random().sparseView();
bc.row_function = ab.col_function;
bc.col_function = std::make_shared<IndexFunction<int>>(
std::begin(IndexC), std::end(IndexC));
MatrixAC ac = ab * bc;
}
Of course a comprehensive implementation would be more involved but since you didn't provide any specific needs, this outline must suffice. I'm not aware of any existing library which provides this kind of function. Most users are content with keeping the index mapping separate from the computation.