The cpse math library

Introduction

The cpse math library was designed in an attempt to prove that a math library that is easy to use is not neccesarily less efficient than hand-written code or a hard-to-use library.

Problems with math libraries

Temporaries

C++ supports operator overloading which makes it easy to write a math library that is easy to use - i.e. has a natural syntax. The problem is that each invoacation of an operator will cause the result of the calculation to be evaluated.

A = B * C;

In the statement above, B * C is evaluated to a temporary variable, which is copied to A. Matrices are usually too big to be copied like this without a noticeable decrease in performance.

Many would argue that if you are multiplying matrices so often that performance is compromised then the number of multiplications is the problem, not the temporaries. The problem also exists for vectors.

v = u * 2 / length(u);

The statement above will create two temporary vectors. More importantly, the statement will be evaluated as if it was written like below.

t1 = u * 2;
t2 = t1 / length(u);
v = t2;

The desired code.

t = length(u);
v.x = u.x * 2 / t;
v.y = u.y * 2 / t;

Is beyond the capabilities of most - if not all - compilers.

Vector versus scalar product

This is admittedly a minor problem compared to what is described above, it is still a problem though.

Once people get used to operator overloading they tend to (reasonably) expect that every operation that is represented by an operator in mathematics uses that operator in C++.

Some people prefer that operator* represents scalar product, some vector product and some neither - to avoid confusion.

Solutions

Delayed evaluation

Both of the problems with temporaries described above can be solved by delaying evaluation of the expression to the final assignment.

Delayed evaluation is achieved by having the operators return what is usually called closures. An extremely simplified example follows.

struct matrix_mul_matrix {
        const matrix &left;
        const matrix &right;
};
matrix_mul_matrix operator*(const matrix &left, const matrix &right);

By parameterizing the closure class with operand types and operation type we get.

template<class Op, class L, class R> struct matrix_binop { ... };

The last returned object contains all information about the expression. Its type will be something nice and readable like

matrix_binop<binop_add, matrix_binop<binop_mul, basic_matrix<4, 4>, basic_matrix<4, 1> >, basic_matrix<4, 1> >; // (A * v + u)

One could write an evaluation function directly for this type. It would likely execute extremely fast compared to naïve code but the set of expressions used is large and unpredictable.

In order to be able to evaluate all expressions we start decomposing the expression tree. If there is an evaluation function for the current expression it is used, otherwise the operands are evaluated. The expression is then recombined with the same operation but with completely evaluated arguments.

The important special case where an expression can be evaluated by looking at a single element in the involved matrices at a time is handled by introducing a member function in all closure objects that returns the value at a specified index in the matrix. No temporaries will ever be used for an expression of this kind.

Vector and scalar product

A potential ambiguity is introduced by having both scalar and vector product use operator*. In real code, the ambiguity can usually be resolved by looking at the entire expression where the operator occurs.

u * v; // ambigous
w = u * v; // vector product
a = u * v; // scalar product

The delayed evaluation mechanism described above provides the required information to resolve the ambiguity.

Note that some ambiguities cannot be resolved.

u = u * v * w; // ambigous
u = dot(u, v) * w; // interpretation 1
u = cross(cross(u, v), w); // interpretation 2

Final results

Using the techniques described above.

A = B * C * D; // 1 temporary
v = A * u; // no temporaries
v = (u * a + w * b) / (a + b); // no temporaries

Problems

There are some problems with the implementation.

A = A * B; // aliasing between result and operand

Aliasing is not allowed. If it were allowed, expression evaluation functions would have to make temporaries.

Matrices and vectors

All objects of type tmatrix<basic_matrix<Rows, Columns> > are fully functional matrices. For convenience, matrix<Rows, Columns = Rows> and vector<Rows> are provided. A vector is a row matrix.

Matrices are stored in column major order. Indexing starts at 0.

Members

Indexing

A(0,1); // element at row 0, column 1
v(1); // (row matrices only) the second element (element 1)

Data access

A.data(); // return a pointer to the internal float array
u.x; // the first element in a 2, 3 or 4 row vector

The named elements x, y, z, w and s, t, u, v are supported.

Operators

operator*

A * a; // matrix * scalar -> matrix
a * A; // scalar * matrix -> matrix
A * B; // matrix * matrix -> matrix
v * u; // vector dot vector -> scalar
v * u; // vector3 cross vector3 -> vector3

Note that matrix * vector is actually matrix * matrix.

operator/

A / a; // matrix / scalar -> matrix

operator+, operator-

A + B; // matrix + matrix -> matrix
A - B; // matrix - matrix -> matrix
+A; // + matrix -> matrix
-A; // - matrix -> matrix

operator<<

std::cout << A << std::endl; // print matrix
std::cout << v << std::endl; // print vector in transposed form
A << 1,2,3,
     4,5,6,
     7,8,9; // assign to matrix

Functions

\todo document functions