Clément Lavoillotte
personal page
Clement's web pages
A simple matrix class in C++
Une classe pour représenter et faire des opérations sur les matrices en C++.
Yet another matrix class in C++
Here is an atempt to provide a simple and correct matrix template class in C++. It is based on the great exemple from the C++ FAQ Lite.
Important note: this class is provided only for educational purpose, and is not meant to be used in a production environment. If ou're looking for production-ready classes, have a look on the Boost C++ libraries. E.g., this class is not optimized for speed by using expression templates.
It provides:
- arithmetic operators (+, -, /, *)
- matrix inversion method (be carefull, the matrix is then replaced by its inverse)
- stream operators (>> and <<)
- is_equal method, with special treatment for float and double
- brackets operator (), to allow things like
Matrix<int> A(3,3); // create a 3x3 matrix of int
A(1,2) = -5; // put -5 in the 2nd row, 3rd column of A (since indexes starts at 0)
All of this in a template class that can be used with all nulber-like entities matching te following criteria:
- operators +, -, *, =, /, ==, !=, << and >> are provided
- autocast from double is available
- entity has a default constructor
- entity supports std::abs (provides an overload)
It also provides I/O operators with the following syntaxes:
- "[nrows x ncols]" and one line per row of tab-separated values for the output operator <<
- blank-separated (spaces, tabs or new lines) values, row-by-row for the input operator >> (it is convenient to use the same syntax as output, ie line-rows of tab-separated values, but it's not mandatory)
Example of use:
#include "matrix.h"
using namespace std;
using namespace mtx;
int main( int argc, char* argv[] )
{
Matrix<double> A (3, 3);
for ( unsigned i = 0; i < m.nrows(); ++ i )
for ( unsigned j = 0; j < m.cols(); ++ j )
A(i,j) = i + j;
cout << A << endl;
}
The matrix inversion method is derived from the corresponding C method of Numerical Recipes in C, Second Edition.
Compiles with gcc4, gcc4-apple and MSVS 2005.
Since it's a template class, all the code fits in a signle header file, matrix.h:
#ifndef MTX_MATRIX_H
#define MTX_MATRIX_H 1
#include <vector>
#include <iostream>
#include <string>
#include <stdexcept>
namespace mtx {
// generic function for equals
template<typename T>
inline bool is_equal( const T& x, const T& y )
{
return ( x == y );
}
// Its specialization for doubles
template<>
inline bool is_equal<double>( const double& x, const double& y )
{
static const double epsilon = std::numeric_limits<double>::epsilon();
const double diff = std::abs( x - y );
return ( diff <= epsilon * std::abs(x) || diff <= epsilon * std::abs(y) );
}
// and for floats
template<>
static inline bool is_equal<float>( const float& x, const float& y )
{
static const float epsilon = std::numeric_limits<float>::epsilon();
const float diff = std::abs( x - y );
return ( diff <= epsilon * std::abs(x) || diff <= epsilon * std::abs(y) );
}
// The Matrix<T> class is a generic 2-dimensional container for numeric elements.
// It can calculate basic operations +, -, *, /, << and >>
// if those elements provide :
// operators: +, -, *, =, /, ==, !=, <<, >>
// autocast from double
// a default constructor
// an overload of std::abs
template<typename T>
class Matrix {
//typdefs
typedef std::vector<T> vector_t;
typedef std::vector<vector_t> matrix_t;
public:
// Constructor
Matrix(const unsigned nrows, const unsigned ncols, const T& init_val = 0);
// No need for destructor, copy constructor or assignment operator
// as std::vector properly handle them
// Accessors
// Access methods to get the (i,j) element:
T& operator() (unsigned i, unsigned j); // not-const subscript operator
const T& operator() (unsigned i, unsigned j) const; // const subscript operator
unsigned nrows() const; // returns the number of rows in the matrix
unsigned ncols() const; // returns the number of columns in the matrix
// Exceptions
// Thrown by constructor if either size is zero
class BadSize : public std::invalid_argument
{ public: BadSize() : std::invalid_argument("Bad size for matrix.") {} };
// Thrown by accessors if i or j is too big
class BoundsViolation : public std::out_of_range
{ public: BoundsViolation() : std::out_of_range("Matrix index out of range.") {} };
// Thrown by operations throw if the dimentions of one operand are not suitable for the operation
class BadOpSize : public std::invalid_argument
{ public: BadOpSize() : std::invalid_argument("Bad size of matrix operand.") {} };
// Thrown by LU decomposition if the matrix is singular
class Singular : public std::invalid_argument
{ public: Singular() : std::invalid_argument("Invalid argument: singular matrix.") {} };
// Functions
Matrix<T>& invert();
private:
// Private functions called by invert()
void lu_decomposition(std::vector<unsigned>& index);
void lu_substitution(const std::vector<unsigned>& index, vector_t& col) const;
// Mermber variable
matrix_t matrix;
};
template<typename T>
inline unsigned Matrix<T>::nrows() const
{
return matrix.size();
}
template<typename T>
inline unsigned Matrix<T>::ncols() const
{
return matrix[0].size();
}
// in the function definition we use (row, col) instead of (i,j) for clarity
template<typename T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
if ( row >= nrows() || col >= ncols() )
throw BoundsViolation();
return matrix[row][col];
}
template<typename T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
if ( row >= nrows() || col >= ncols() )
throw BoundsViolation();
return matrix[row][col];
}
//////////////////////////////////////////////////
// Replace M by the LU decompusition of a rowwise-permutation of itself, and
// takesa vector of unsigned int that will become the row-permutations index
template<typename T>
void Matrix<T>::lu_decomposition(std::vector<unsigned>& index)
{
// We only perform LU decomposition on squared matrices
if( ncols() != nrows() )
throw typename Matrix<T>::BadOpSize();
// hence n is the size of the Matrix
unsigned n = ncols();
register unsigned i, j, k; // for clarity we won't re-declare them every loop
// for speed we declare them as register
unsigned i_pivot; // a row index used for permutation
// other temporary objects declared here to avoid multiple contructor calls
T tmp, max, sum;
// This will store the the maximum of each row (to be used for the pivot search)
std::vector<T> max_inv( n );
index.resize( n ); // The row-permutation index has one entry for each row
using std::abs;
// Find the maximum of each row and store its inverse in inv_max
for( i = 0; i < n; ++i ) {
max = 0;
for( j = 0; j < n; ++j ) {
if( abs( matrix[i][j] ) > abs( max ) )
max = matrix[i][j];
}
if( is_equal<T>(max, 0) ) // Largest element == 0 implies singular matrix
throw typename Matrix<T>::Singular();
max_inv[i] = max;
}
// Crout's method to solve N≤+N equations with N≤ unknowns :
// remplace each element of the row by the lower or upper matrix elements
for( j = 0; j < n ; ++j ) {
for( i = 0; i < j; ++i ) { // first part: 0 <= i < j
sum = matrix[i][j];
for( k = 0; k < i; k++ )
sum = sum - matrix[i][k] * matrix[k][j];
matrix[i][j] = sum;
}
max = 0;
i_pivot = j; // not mandatory just to prevent compiler warning
for( i = j; i < n; ++i ) { // seconde part: j <= i < n (with pivoting)
sum = matrix[i][j];
for( k = 0; k < j; k++ )
sum = sum - matrix[i][k] * matrix[k][j];
matrix[i][j] = sum;
// search for the largest element
tmp = sum * max_inv[i];
if( abs(tmp) >= abs(max) ) {
max = tmp;
i_pivot = i;
}
}
if(j != i_pivot) { // if we need to permute row
std::swap(matrix[j], matrix[i_pivot]); // swap vectors is fast
max_inv[i_pivot] = max_inv[j]; // actualize the maximum inverse
}
index[j] = i_pivot; // index the column number
if( is_equal<T>(matrix[j][j], 0) ) // Largest element == 0 implies singular matrix
throw typename Matrix<T>::Singular();
// Divide the remaining elements in the columns by the pivot
if( j < n-1 ) {
tmp = 1.0 / matrix[j][j]; // only one division, several multiplications
for(i = j+1; i < n; i++)
matrix[i][j] = matrix[i][j] * tmp;
}
} // next column j
}
// One the matrix is LU-decomposed, applies forward & backward substitution
// Optimized for vectors with many zeroes
template<typename T>
void Matrix<T>::lu_substitution(const std::vector<unsigned>& index, vector_t& col) const {
T sum;
unsigned n = ncols();
register unsigned i, j;
bool has_non_zero = false;
unsigned i_non_zero = 0;
// forward substitution + permutations
for( i = 0; i < n; ++i ) {
// applies the permutation to solution vector
sum = col[index[i]];
col[index[i]] = col[i];
// forward substitution for non-zero columns
if( has_non_zero ) {
for( j = i_non_zero; j < i; ++j )
sum = sum - matrix[i][j] * col[j];
}
else if( ! is_equal<T>(sum, 0) ) { // if we encounter a non-zero element
has_non_zero = true; // we'll apply the forward substitution for the next rows
i_non_zero = i; // starting from the current row number
}
col[i] = sum; // place the solution in the vector;
}
// back substitution
for( i = n; i >= 1; --i ) {
sum = col[i-1];
for( j = i; j < n; ++j )
sum = sum - matrix[i-1][j] * col[j];
col[i-1] = sum / matrix[i-1][i-1];
}
}
// Replace the current matrix by its inverse and returns a reference to itself.
template<typename T>
Matrix<T>& Matrix<T>::invert() {
unsigned n = ncols();
std::vector<unsigned> index( n );
matrix_t identity( n, vector_t( n, 0 ) ); // Note : identify is stored by COLUMN, and NOT by ROW like the current matrix
// Perform LU decomposition only once
try {
lu_decomposition(index);
}
catch(...) {
throw;
}
// Perform the substitution for each column of the identity matrix
// ie identity becomes the inverse of the current matrix
for( unsigned i = 0; i < n; ++i ) {
identity[i][i] = 1; // place the 1 in the diaginal
lu_substitution(index, identity[i]);
}
// We can now exchange our values with the identity matrix which
// has been replaced by our inverse, by doing a simple transpose
for( unsigned i = 0; i < n; ++i )
for( unsigned j = 0; j < n; ++j )
matrix[i][j] = identity[j][i];
return (*this);
}
//////////////////////////////////////////////////
template<typename T>
Matrix<T>::Matrix(const unsigned nrows, const unsigned ncols, const T& init_val)
: matrix (nrows, vector_t(ncols, init_val)) // initialize all vectors
{
if( nrows == 0 || ncols == 0 )
throw BadSize();
for( unsigned i = 0; i < nrows; ++i )
matrix[i].resize(ncols);
}
// Matrix operations.
// In comments we use the notation: Matrix[number of rows * number of columns]
// Addition : A[m * n] + B[m * n] = C[m * n]
template<typename T>
Matrix<T> operator+( const Matrix<T>& A, const Matrix<T>& B )
{
if( A.nrows() != B.nrows() || A.ncols() != B.ncols() )
throw typename Matrix<T>::BadOpSize();
Matrix<T> C( A.nrows(), A.ncols() );
for( unsigned i=0, nrows=C.nrows(); i < nrows; ++i )
for( unsigned j=0, ncols=C.ncols(); j < ncols; ++j )
C(i,j) = A(i,j) + B(i,j);
return C;
}
// Substraction : A[m * n] - B[m * n] = C[m * n]
template<typename T>
Matrix<T> operator-( const Matrix<T>& A, const Matrix<T>& B )
{
if( A.nrows() != B.nrows() || A.ncols() != B.ncols() )
throw typename Matrix<T>::BadOpSize();
Matrix<T> C( A.nrows(), A.ncols() );
for( unsigned i=0, nrows=C.nrows(); i < nrows; ++i )
for( unsigned j=0, ncols=C.ncols(); j < ncols; ++j )
C(i,j) = A(i,j) - B(i,j);
return C;
}
// Matrix multiplication:
// A[m * p] * B[p * n] = C[m * n]
template<typename T>
Matrix<T> operator*( const Matrix<T>& A, const Matrix<T>& B )
{
if( A.ncols() != B.nrows() )
throw typename Matrix<T>::BadOpSize();
unsigned m = A.nrows();
unsigned n = B.ncols();
unsigned p = A.ncols();
// create the matrix of size m*n and initialize all elements to 0
Matrix<T> C( m, n, 0 );
for( unsigned i = 0; i < m; ++i )
for( unsigned j = 0; j < n; ++j )
for( unsigned k = 0; k < p; ++k )
C(i,j) = C(i,j) + ( A(i,k) * B(k,j) );
return C;
}
// Matrix division
// C[n * n] = A[n * n] / B[n * n] is equivalent to
// C = A * B.inverse() except that only a copy of B is inverted
// The multiplication code is reapeated to gain -1 Matrix instance
template<typename T>
Matrix<T> operator/( const Matrix<T>& A, const Matrix<T>& B )
{
if( A.nrows() != B.nrows() || A.ncols() != B.ncols() || A.nrows() != A.ncols() )
throw typename Matrix<T>::BadOpSize();
unsigned n = A.nrows();
Matrix<T> C( n, n ), INVB = B;
try {
INVB.invert();
}
catch(...) {
throw;
}
// multiply A by the inverse of B
for( unsigned i = 0; i < n; ++i )
for( unsigned j = 0; j < n; ++j )
for( unsigned k = 0; k < n; ++k )
C(i,j) = C(i,j) + ( A(i,k) * INVB(k,j) );
return C;
}
//////////////////////////////////////////////////
// Write the matrix m in the output stream s.
// The first line is: [number_of_rows x number_of_columns]
// then it writes 1 line per matrix row, separating the values with tabs.
//Ex: [3 x 3]
// -1 5 0
// 7 -2 1
// 4 1 6
// This function need operator << to be available for the type T,
// otherwise it'll cause a compile-time error.
template<typename T>
std::ostream& operator<<( std::ostream& s, const Matrix<T>& M )
{
s << "[ " << M.nrows() << " x " << M.ncols() << " ]\n";
for( unsigned i=0, nrows=M.nrows(); i < nrows; ++i )
for( unsigned j=0, ncols=M.ncols(); j < ncols; ++j )
s << M(i,j) << (j == ncols-1 ? '\n' : '\t');
return s;
}
// Read each elements of the matrix m from the input stream s.
// As '\t' and '\n' are white spaces (hence input delimiters), it is
// convenient to input the matrix as 1 line = 1 row of tab-separated values.
// Note: the dimensions of the matrix are already entered in m.
// This function needs operator >> to be available for the type T,
// otherwise it'll cause a compile-time error.
// The error handling is leaved to the underlying type, so on error all
// the remaining elements will keep their actual values, and the ios fail
// flag will be maintained, allowing a check like: while(cin >> m) {...}
template<typename T>
std::istream& operator>>( std::istream& s, Matrix<T>& M )
{
for( unsigned i=0, nrows=M.nrows(); ( i < nrows && s); ++i )
for( unsigned j=0, ncols=M.ncols(); ( j < ncols && s); ++j )
if( !(s >> M(i,j)) )
std::cerr << "Error while reading matrix, at row " << i << " column " << j << "." << std::endl ;
return s;
}
} // end namespace mtx
#endif
If you have any question or comment regarding this code, feel free to email me.