Mircea Ispas
Mircea Ispas

Reputation: 20780

C++ operator overloading doesn't respect math requirements

I have to write a math library for internal use. I started to look at different implementation from open source libs and I found some weird things on operator overloading - they don't respect mathematical/logical requirements

Example 1: Irrlight Matrix (http://irrlicht.sourceforge.net/docu/matrix4_8h_source.html)

Example 2: GLM Matrix (http://glm.g-truc.net/)

Similar examples can be found on different types in almost all libraries I've checked. I've read in Elements of Programming by Alexander Stepanov that one shouldn't change operators meaning nor implement them when they don't make sense, but I see many example where these guidelines are not respected.

Is this a good practice? If yes can you please give me arguments. If no, why everybody does this?

EDIT:

I'll try to get a better example:

template <typename U>
GLM_FUNC_DECL tvec4<T> & operator*=(tvec4<U> const & v);

with this implementation

template <typename T>
template <typename U> 
GLM_FUNC_QUALIFIER tvec4<T> & tvec4<T>::operator*=
(
    tvec4<U> const & v
)
{
    this->x *= T(v.x);
    this->y *= T(v.y);
    this->z *= T(v.z);
    this->w *= T(v.w);
    return *this;
}

Can you please explain what is the meaning of this in the context of mathematical vectors(not colors or points or... something else that can be represented as 4 elements array)?

Upvotes: 1

Views: 311

Answers (3)

Francis Cugler
Francis Cugler

Reputation: 7905

Having a generic math library is one thing, however with the glm math library it is designed with the convention of GLSL in mind. It is setup this way so that when you write shaders in GLSL, the use of the glm math library is already familiar to you and the work flow is much smoother.

When using shaders and rendering on hardware versus the CPU, all of you data sets are stored on the video card's ram. Matrices are not just used for doing basic mathematic transformations such as translation, rotation, scaling, skewing etc., they can also be used to do other things as well. When you program using shaders on modern video cards they work very well in parallel processes due to their architecture.

Since mat4x4 is a template type what is to stop you from storing pointers to a function call or function pointers?

You could have say a 4x4 matrix where there is a function pointer saved into each index and the matrix acts as a reference to a function pointer to do the same work in multiple threads while in parallel. Then having a second 4x4 matrix with another set of function pointers would act the same way. What would the use of an overloaded operator+() serve here?

Using mat4x4 A + mat4x4 B doesn't equal mat4x4 C that you would expect. Instead of adding each element's data in the matrix to give you a new matrix, the overloaded operator that you would define would let you invoke the call to the function pointer in B right after A leaves scope. Allowing you to concatenate multiple function calls on a specific data set, while working in multiple threads in parallel.

Let us say you have a texture of size w x h and each pixel value represents normal values for lighting on a 3D terrain. We then want to perform one operation on each pixel data --> (normal values), then another right after each other in that exact order. Let us say that this function pointer foo() does one set of operations on each normal and then function pointer goo() does a different operation on these normals to give you the desired result.

So matrix A has 16 cells and each of these represents a reference to the function pointer to foo(), and matrix B does the same thing for goo(). Now, for this implementation in the engine, the designer would have to write their own overloaded operator+() function so that it would invoke the call to foo() in parallel threads on texture T (normal data) and immediately after it would then invoke the function call to goo() on T in parallel and would probably return a vector or char[x * h] back with the results for the shader to use.

This would have 16 threads each processing the same operations working on the same data set. For every pixel entry in T (normal values) that is W x H; each call to foo and goo would work on (W x H) / 16, a subset of the data type. Since these operations are being done on a pixel by pixel basis this is much faster then running through a double for loop and calling one method, returning out and doing another double for loop or doing a double for loop and calling one method after another on the data set.

The return type of this Mat4x4 would not be a translation as you would expect but a data set that was worked on using two function calls in parallel. Now in this example the overloaded operators would have to be written by the programmer since it isn't exactly included in the library.

Most libraries are designed to be a loosely general and generic with most of all the common functionality you would expect. No library is perfect, and some are better then others. Each library has their strengths and faults.

In the above example, you may not be able to do this directly from the provided mat4x4 class, but in the creation of your source code for such an engine, you could inherit from the glm::mat4x4 class and create your own. It might look this

namespace glm {
template<typename T>
class mat4x4 {

};
} // namespace


// In your class definition you might have
#include <glm/mat4x4.hpp>

template<typename T, typename FuncPtr>
class myMat4x4 : public glm::mat4x4 {
private:
    std::vector<T> m_vData;
    FuncPtr        m_funcPtr;
public:
    myMat4x4();
    explicit myMat4x4( std::vector<T>& vData ); // std::vector<T> has the data
    // from texture tex1 that was previously stored.
    const std::vector<T>& operator+( const myMat4x4<T,FuncPtr>& rhs );    
};

And through template specialization the only type acceptable by the second typename would be a pointer to a function call. In this situation the type T passed in would have to also match the type that method pointed to by FuncPtr accepts. In your constructor you would have to set the saved FuncPtr to each element of glm::mat4x4 but within your derived class. Your overloaded operator might be like:

template<typename T, typename FuncPtr>
std::vector<T>& myMat4x4<T,FuncPtr>::operator+( const myMat4x4<T,FuncPtr>& rhs ) {
    // Invoke this->m_funcPtr on this->m_vData save results back into
    // this->m_vData where each this[m][n] element is called on m_vData[i]
    // meanining this[0][0] FuncPtr works on m_vData[0] T, this[0][1] FuncPtr works on m_vData[1] ...
    // until this[m][n] works on m_vData[last] then make sure m_vData is updated correctly and valid.
    // Next would be to invoke rhs.m_funcPtr on this->m_vData in the same fashion and save data into this->m_vData.
    // Here the rhs myMat4x4 doesn't have anything saved into its (rhs)m_vData since it used the default constructor.
    // But it does have the pointer to goo() saved in rhs.m_funcPtr
    // Check validity of data set if everything is okay and no errors or exceptions, now we can just return this->m_vData
} 

As you can see, in this example of a Matrix class, it doesn't follow the mathematical rules of matrix operation for doing transformations. But it is a means to structure a parallel multi threaded operation on a data set which is beneficial to how GPUs are structured.

Upvotes: 0

Useless
Useless

Reputation: 67743

It's bad practice to overload operators in ways that genuinely don't make sense, certainly.

However, it isn't at all clear from your question that these overloads really don't make sense: rather, it seems like they don't make sense for your specific use case.

A library being more general than you need isn't a bug, and libraries often err on the side of generality.

Now, if a library provides a generic Matrix, and you only want it for transformations, it might be reasonable for the library to also provide a TransformationMatrix which provides only that subset of matrix operations that are sane for transformations. Indeed, that sounds like a pretty good idea, although it might come at the cost of considerable extra complexity in the library's type system.

Upvotes: 5

Bartek Banachewicz
Bartek Banachewicz

Reputation: 39380

Yes, these operators are fully justified.

In general, matrix addition is well-defined mathematical operation, so your point about "not respecting math requirements" is simply wrong. Matrix multiplication is not commutative, so you shouldn't expect that either.

As for actual usage

I don't know about Irrlicht, but in GLM it's because of the fact that matrices aren't used solely for rendering.

GLM types are modeled on the basis of GLSL matrices; the fact that these can store 3D transformations is irrelevant, as shaders might use them to store arbitrary data. Then, addition and subtraction can be a valid operation for what one might use them for, and "3D engine context" is just one possible context.

Upvotes: 7

Related Questions