Class design, kinda math specific.

This topic is 1756 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

Recommended Posts

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...

Ok, so assuming I got the cross product order correct (I ALWAYS forget... ) 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.

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.
Edited by All8Up

Share on other sites

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.

Edited by Cornstalks

Share on other sites

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.

Share on other sites

Hmm.....  It is an idea but pretty non-standard from the code bases I've worked with.  Not saying it is wrong, just thinking as I type here.  In the one case I definitely agree that overloading operators with non-standard meanings is generally a bad idea.  Unfortunately I've gotten so damned used to the caret (^) meaning cross product it has become yet another thing I should re-evaluate.  The idea of normalize(d) has me wondering though.  Is the semantic change enough to prevent errors and are the signatures hardened against the "oops" cases?

I think the solution I've presented has all the same results but with only two functions?  I'm trying to think of other variations and I might be too bogged down since I've given this a fair amount of thought today.

* edit

A bit more thought after reading the other thread.  I still think that "Normalize" is a good name for both variations.  The reasons for mutable and const are all valid and how to deal with extra mutated results.  I believe my proposal answers all the suggested items though and the only real thing is if the helper should be called "Normalized" instead of just "Normalize"?  It is nice to know I'm not being too pedantic in this, seems other folks care also.

Edited by Hiwas

Share on other sites

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...

Share on other sites

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.

Yeah, the second thread is the one that got me thinking about it enough to post the original question/description. :)

Share on other sites

Thanks for the feedback folks.  I'm not sure how much I'll follow the other threads of thought, well only in the case of using two names, but pretty sure the results all basically end up at the same place.  The case of the operators, that's going to be difficult, I am just SOOOOO used to the caret meaning cross product it would take a while to break that habit.  But, it would be possible to move such an operator to a "horrible_helper.hpp" where it defines such things as external helper operators.  That portion I can deal with on my own, making sure I'm not trolling down a crap path, very much appreciate the feedback that I'm not insane by worrying about such trivial items.

Edited by Hiwas

Share on other sites
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.

Share on other sites

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.

Yup.  But I believe I have the outline written the way you prefer unless I made a goof somewhere.  The "member" is the mutator with a void return and the friend/helper is the take something and mutate it returning the result version...  In fact I'm almost positive I wrote it correctly because the non-member doesn't even have to be a friend function to do it's job.  I could be wrong, pretty sure I'm not though. :)

Share on other sites
Its just, are you sure you need the same functionality more than once? Why not just have a function and assign the return value?

Share on other sites

Its just, are you sure you need the same functionality more than once? Why not just have a function and assign the return value?

Ah, good point.  The difference is in the utility of the class.  If I have only the member function then an equation such as the following becomes impossible:

Vector3f negVec = -Normalize( other );


You have to write:

Vector3f otherNormalized = other;
otherNormalized.Normalize();
Vector3f negVec = -otherNormalized;


The point here is to avoid easy errors but also to prevent the C like verbosity required without the helper.

Writing "equations" be they based on float or a vector should not be "too" different given that the basic math operations are all the same.  Results are completely different and an equation in one type of math is completely different in another but the idea is that: "a = 1+2 == 3" are writable in both cases, though in the case of vectors the result is actually {3, 3, 3}...  This is a bit different than the "normalize" function but basically extending the concepts of floats to higher dimensions.  Figuring out equations in 1D often expands to 3D, why should the math required be typed in differently?

Share on other sites

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.

Share on other sites

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.

err... You sure about that code? :)  Anyway, without operator overloads, Java is a really bad language to compare to in this area.  Without overloads, Java doesn't allow a lot of the reasons for this subject matter.  I.e. writing a simple equation which can be applied to multiple dimensions: a+b, be they floats, vector 2 or 3 etc.

Share on other sites

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.

Share on other sites

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:

Sorry, it was meant as a joke at the time. :)  Guess it didn't come across correctly....

public static DataArrayFloat normalize3f ( DataArrayFloat vec, DataArrayFloat dest )
{
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.

In the context of normalize as a single function, operators are not required as you say.  I was just meaning that when placed inside the equational context where the operators are used, C++ normalize has different usages in different cases.  The simplified solution we talked about just means 2 versions, one is the real version and the other is basically just syntactic sugar for use when placed into a larger equation as context.

I hope this makes a bit of sense.  I can't think of a really good way to describe the intention here.  It's almost the old C manner of doing math which is similar to the method of doing it using Java.  This discussion is very much intended to the math overloads of operators and how a normalize function fits into that context.  Hence Java is not really a good example when trying to make the comparisons and use/cases which I posted.