• Create Account

Class design, (kinda math) part two.

Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

#1AllEightUp  Moderators   -  Reputation: 3887

Like
0Likes
Like

Posted 16 April 2013 - 10:45 PM

Hi folks, with a bit of time off traveling and an offshoot article about bit fields, time to get back to the plan I had.

In the original post about class design (link) I was just targeting a type of functionality which, if done poorly, can lead to trivial bugs that could have been avoided.  I think the responses were all valid and of course I made a couple changes, though I'm still on the fence about one of them because it hurts my world view and I have a different bit of thinking on it.  (Yeah, "Normalize" versus helper "Normalized".. )

Anyway, stripping down the class in question, I ended up with the following (code/comment stripped to get an idea of how 'self documenting it is' and raw feedback before I make comments below and hit the 'reasons'):

class Vector3fv
{
public:
static const size_t kXIndex = 0;
static const size_t kYIndex = 1;
static const size_t kZIndex = 2;

Vector3fv();
Vector3fv( const Vector3fv& rhs );
Vector3fv( float x, float y, float z );

Vector3fv( SIMD::Vec4f_t rhs );
operator SIMD::Vec4f_t();

float X() const;
float Y() const;
float Z() const;

void X( float x );
void Y( float y );
void Z( float z );

float operator ()( size_t index ) const;
void operator ()( size_t index, float v );

void Set( float x, float y, float z );

const Vector3fv& operator =( const Vector3fv& v );

Vector3fv operator -() const;
Vector3fv operator +( const Vector3fv& rhs ) const;
Vector3fv operator -( const Vector3fv& rhs ) const;
Vector3fv operator *( float rhs ) const;
Vector3fv operator /( float rhs ) const;
float operator *( const Vector3fv& rhs ) const;

void operator -=( const Vector3fv& rhs );
void operator +=( const Vector3fv& rhs );
void operator *=( const float rhs );
void operator /=( const float rhs );

void Normalize();
void Cross( const Vector3fv& rhs );

static const Vector3fv kZero;
static const Vector3fv kXAxis;
static const Vector3fv kYAxis;
static const Vector3fv kZAxis;

private:
SIMD::Vec4f_t mData;
};

inline float Dot( const Vector3fv& lhs, const Vector3fv& rhs );
inline Vector3fv Normalize( const Vector3fv& rhs );
inline Vector3fv Cross( const Vector3fv& lhs, const Vector3fv& rhs );
inline float Magnitude( const Vector3fv& lhs, const Vector3fv& rhs );
inline Vector3fv operator *( float lhs, const Vector3fv& rhs );


Edited by AllEightUp, 16 April 2013 - 10:52 PM.

#2AllEightUp  Moderators   -  Reputation: 3887

Like
0Likes
Like

Posted 16 April 2013 - 10:53 PM

Sorry, something is wonky here.  Everything past the code refuses to post so:

Taking points top to bottom covering a couple design items:

First, this is a vectorized 3D vector (I.e. SIMD instruction set for whatever CPU) which I hope is obvious.  The only non-obvious is the 'v' postfix.  I use a modification of Open GL naming convention, f means it is a single precision float and the v is a re-dedication of pointer to array 'v' to mean it is vectorized in this case.  Not original, not outstanding, just easy and I like easy.  'k' for constants, old habit, probably could drop it.  Case, same thing, old habit, like it though since using std and Math won't compile error on synonyms.

The first contentious item is the pair of converters.  Construct from a SIMD type and convert to SIMD type and they are intentionally not explicit.  I'm torn on this to a degree but in the long run I settled on non-explicit because I want to encourage passing things around as the fundamental type and not this light wrapper.  Unlike most other conversion operations this one comes without a cost and more importantly passing by the fundamental type and not the wrapper is considerably more efficient in many ways.  (Wait for the article for a discussion of this. )  I'm open to further discussion but if you are doing a SIMD library, you might as well encourage the efficient usage I believe.

The next contentious item is usually a pretty nasty one.  Accessors for X, Y and Z, no direct access allowed.  I don't do this for C++ class "purity" though, I'll drop kick purity in a heartbeat if I believe it is justified.  Instead I use the accessors as a future preventative item for debugging.  I can realistically argue that *every* game I've ever worked on has eventually had problems with NAN/INF getting into vectors and they were often nightmarish to track down and fix "because" of direct access to members which bypassed (yeah a purist point) the ability to add in debug code later to help track the problem.  Getting used to 2 extra characters to type is trivial day to day, saving major time debugging is priceless to me.

Index operators.  Errf..  This one has me torn.  If I supply the normal index operator of '[]' the only way to do assignment is via returning a reference.  I can/have coded up an intermediary object which can represent a reference into a SIMD vector element, but it is considerably more code (just lots of repeated boilerplate crap), not all that efficient and allows retention of the index object in ways very undesirable.  Using the paren operator is a bit of a perversion of the syntax but in this way I can maintain the strict access control and not introduce the helper class to represent a reference.  I could of course use standard '[]' for getter and '()' for the setter, but that struck me as wrong also.  I'm very interested in comments about this.  Of course it could be asked if this should exist at all, well kd-tree's, octree's, sweep and prune will all hate you if you don't supply it.  Those are rather complicated systems which use axis indexing all over the place, not supporting index'd access is not an option in my opinion.

Set is just syntactic sugar for "X( x ); Y( y ); Z( z );".  It avoids bugs in the cases you really do set all three members at the same time.  No other reason for it's inclusion.

The dot product operator.  Not sure how often this is argued but given it is a unique input and return type, and math texts use multiplication of scalars and vectors differently yet with the same symbol for 'multiply', I figure it is a good overload.  I'm open to discussion.

Members, why so limited?  Well, part of the intention is to be as clean as possible and not to present inconsistency in the supplied interface.  So, why is magnitude a helper and not a member?  Because all the members are currently limited to those which are non-const, return void, and need explicit access to the member data to perform their function.  Magnitude (length) is just the square root of the vector dotted with itself so it is not a member function, just a syntactic helper to reduce typing.  This may be taking minimalist to an extreme but it does present pretty solid consistency in all cases given that the helpers are all cases which can be computed with the minimal set of operations and no member access.

The static initializers.  A case where I'm drop kicking all class purist thinking...  The default constructor does *NOT* initialize to zero.  I have reasons for this I'll explain in the article if you don't already know them.  I keep rechecking the reasons for this every new compiler and it just keeps remaining true.  I think of vectors as a fancy float anyway, so if I don't initialize it it is undefined, just like a float.

OK, that's some of the reasoning, hit me with yer best shot.

Edited by AllEightUp, 16 April 2013 - 11:49 PM.

Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

PARTNERS