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.
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.
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.
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.
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
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
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.
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.
A(0,1); // element at row 0, column 1
v(1); // (row matrices only) the second element (element 1)
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.
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