Skip to main content
GameDev.net gamedev.net
🔒 Locked

Class design, kinda math specific.

Started by All8Up Mar 31, 2013 at 9:46 PM 13 replies 2.8k views
Original Post
All8Up
All8Up

Hi folks, continuing on with re-evaluating a bunch of different things in code as preparation to write another article, a fun one came up which actually someone asked a related question about involving statics. While the question was just "why" you would use it, it applies to the bit I'm considering at this moment. This may be better suited for the math forum's as it is related to math classes but it really is a general programming topic in terms of good class design. Anyway, the basic idea is picking on a single function in a 2d or 3d vector class and considering the ups/downs and possible use cases for the function. While not absolutely critical, I've been re-evaluating it in regards to C++11 (not much change) and common bug scenarios, plenty. The function is simple: "Normalize".

OK, many may be asking just how complicated could "Normalize" be to write. Well, surprisingly complicated if you want to avoid the most likely set of bug producing use cases. I normally re-evaluate how I code every couple years in order to break bad habits I've fallen into (hence the question about includes using brackets versus quotes, seems I'm doing OK in reality) and to see if I've learned anything over that time which could be applied and made better. (Hopefully forming a new habit.) In this case I was working up the preliminaries to extending on the articles I've been writing by attacking a cross platform vectorized math library using SSE and Neon as targets with a fallback to non-vectorized code as required. (I.e. SSE is pretty much a given anymore, Neon is most definitely not.)

Anyway, let's produce some use cases and questionable results:


Vector3f  toTarget = target - source;
Vector3f  upVector = Vector3f::kYAxis;
Vector3f  rightVector = toTarget.Normalize() ^ upVector;

Make two assumptions from here out please: toTarget is not nearly equal to upVector and unit vector is give or take a certain amount of error around 1.0f. Otherwise I'll be describing possible edge cases more than getting to the point... smile.png

Ok, so assuming I got the cross product order correct (I ALWAYS forget... smile.png) I should end up with unit vectors of up and right. Some libraries I've worked with would have a side effect, toTarget would have been normalized in that code. I despise side effects so any behavior which ends up with toTarget being normalized is out the door, so if I want this code to work as written the signature has to be: "Vector3f Normalize() const". It could be non-const but that defeats self documentation in saying *this* is not changed after the call, so non const is also out the door.

Second use case is a two parter.


Vector3f incidentVector = -toTarget.Normalize();

Again, no side effects allowed so the above change works and we get the negated unit vector as desired. Now, in "documenting" code I often break up equations, this would not normally be a candidate but assuming this were some big nasty equation with lots going on that needs to be explaining, I may break the code into the following pieces:


Vector3f incidentVector = -toTarget;
incidentVector.Normalize();
Do you see the bug? I have done this sort of thing many times over the years and seen many others do the same thing. The normalize call no longer "does" anything since it is const and not modifying "this" without an assignment is a nop. Some compilers will give you warnings in this case and prevent you hunting the bug, but not all notice it. Not to mention a lot of code bases turn off "unused return value" warnings since there are a pretty numerous set of cases where it is perfectly valid to ignore return values.
This leads to two conflicting desires, one is to maintain easy to use code and the other is to prevent silly errors such as case two.
So, I was thinking I'd use the following signatures:

class Vector3f
{
public:
   void  Normalize();
   friend Vector3f Normalize( const Vector3f& );  // Optional, could just have the helper
  // defined as: { Vector3f result = in; result.Normalize(); return result; }  which most
  // optimizers with do well with.  (NOTE: C++11 I believe the return would be a &&,
  /// still exploring that bit in detail as part of my re-evaluation..)
};
Given this you have to modify the first use case to use "Normalize( toVector )" since there is no return to work on from the member function. The second use case works as intended and the easy to refactor into a bug issue goes away.
Now, where all this long winded discussion of a single function ends is: "what am I missing"? I've fixed a recurring problem and don't see any further problems which the compiler won't catch and error on. But, I might be too deep in the weeds of one variation to consider the "other" use cases which would lead this to producing yet other common mistakes. I've tried but can't see any real downfalls other than annoying folks used to the more common signatures.
I don't expect a perfect answer, I just don't want to give a poor one if I write an article including this. smile.png
OOPS: Forgot to mention why I was considering the static variation, instead of a helper I was possibly considering using a static "static void Normalize( Vector& )" instead of the void return member. I.e. "Vector::Normalize( toVector )" as an alternative. I think the member+helper works better without the extra typing.
Cornstalks
Cornstalks

We had a thread about this once, and one thing that someone suggested (which I started doing) was using normalized to represent the non-mutating normalize function.

That is:

v.normalize(); // mutating function, modifies v
v.normalized(); // non-mutating function, does not modify v (returns a new vector)

Another suggestion someone brought up was to use free functions instead of member functions. So then I do something like:

// Personally, I hate overloading operators for cross and dot
// (if my keyboard doesn't have the mathematically right symbol, I write it out)
u = cross(a, b);
v = dot(a, b);
w = normalized(a);

In then end, I combine these two. Free functions are always non-mutating (and in ambiguous cases, the past-tense form of the word is used). Member functions that mutate do not use the past-tense. Member functions that obviously don't mutate are allowed (like magnitude()). Member functions that don't modify the object, but is not obvious, are not allowed (like a normalized() member function). Something like:

a = normalized(b); // b is not modified
b.normalize(); // b is modified (note: I don't define b.normalized() (i.e. a non-mutating member version of normalize))
c = cross(a, b); // a and b are not modified
c = dot(a, b); // a and b are not modified
c = -a; // a is not modified

On top of this, I make the b.normalize() function return void, so it's clear it's meant to be called for its side effects and can't be accidentally used in the same place that the free normalized() function can.

Edit: and for those curious, I'm intentionally not using code tags because they're too frustrating to work with.

Servant of the Lord
Servant of the Lord

We had a thread about this once, and one thing that someone suggested (which I started doing) was using normalized to represent the non-mutating normalize function.

Heh, I just linked to the same thread in another thread less than an hour ago.

Cornstalks
Cornstalks


We had a thread about this once, and one thing that someone suggested (which I started doing) was using normalized to represent the non-mutating normalize function.

Heh, I just linked to the same thread in another thread less than an hour ago.

Ha that was a good thread, but I was actually thinking of this thread (and Sneftel's response). I can't remember which thread someone mentioned the free-functions in, though...

wintertime
wintertime
Using a method when you actually want a function that returns a transformed value seems unintuitive. At first it looks like its modifying the object and then as a byproduct returning it again.
wintertime
wintertime
Its just, are you sure you need the same functionality more than once? Why not just have a function and assign the return value?
TheChubu
TheChubu

I'm actually pretty unsure of the speed/clearness/etc of some of the decisions on my math library (in Java) but after going around 3 or so designs, my latest iteration does this:


public static Vector3f normalize( Vector3f vec, Vector3f dest )
{
 
//normalize vec, store in dest.
 
return dest;
}

Instead of writing 2 or 3 similar methods, I wrote only one that can be wired to have different results. If you don't want your vec to be changed, send a different one via the "dest" parameter. If you don't mind your vec changed, call the method with vec on both parameters.

You can call it with an anonymous object if you want since it returns the result. (ie, norm = normalize( vec, new Vector3f() ) ).

I don't think its the most performant implementation. It probably can't be used freely with some operations (matrix multiplication of the top of my head) but it does what I wanted to in a single method instead of using more.

"I AM ZE EMPRAH OPENGL 3.3 THE CORE, I DEMAND FROM THEE ZE SHADERZ AND MATRIXEZ"   My journals: dustArtemis ECS framework and 
TheChubu
TheChubu

What do you mean about if I'm sure? I don't remember C++ syntax (probably missing a :: or something like that) The complete method is this one:


public static DataArrayFloat normalize3f ( DataArrayFloat vec, DataArrayFloat dest )
{
float[] vecArray = vec.getArray(),
destArray = dest.getArray();
 
float norm = 1.0f / (float)(Math.sqrt(vecArray[0] * vecArray[0] + vecArray[1] * vecArray[1] + vecArray[2] * vecArray[2]));
 
destArray[0] = vecArray[0] * norm;
destArray[1] = vecArray[1] * norm;
destArray[2] = vecArray[2] * norm;
 
return dest;
}
As I implemted it, you can have a normalize method for arbitrary vector length by doing a for loop instead.

Anyway, why specifically do you need operator overloading for a normalize function? I reckon addition and substraction operators come in handy (see up there, no power ^ operator either heh) but I don't see its use for normalization.

"I AM ZE EMPRAH OPENGL 3.3 THE CORE, I DEMAND FROM THEE ZE SHADERZ AND MATRIXEZ"   My journals: dustArtemis ECS framework and 

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.