PDA

View Full Version : Using a class instead of array



greg2
05-16-2007, 10:10 AM
Hi,

I am writting an utility library similar to D3DX for my new OpenGL engine.

I have noticed that a class can be given to OpenGL's functions like glLightfv. The first member of the class will correspond to the elements of the array. It works, but is it a good idea ? Will it not be slower ?

Here is an example:

My class:

class GLGXVECTOR3{
public:
union{
float x;
float u;
};
union{
float y;
float v;
};
union{
float z;
float r;
};

GLGXVECTOR3();
GLGXVECTOR3( float x , float y , float z );
~GLGXVECTOR3(){};

GLGXVECTOR3 operator+ (GLGXVECTOR3 &B);
GLGXVECTOR3 operator- (GLGXVECTOR3 &B);
float operator* (GLGXVECTOR3 &B);
GLGXVECTOR3 operator* (float factor);
GLGXVECTOR3 operator/ (float factor);
};The way i use it:



GLGXVECTOR3 Loc(0.0f,100.0f,0.0f);
glLightfv(GL_LIGHT0, GL_POSITION,(float*)&Loc);
Thanks.

Overmind
05-16-2007, 10:40 AM
It will not be slower, but it's dangerous. You never know for sure how your compiler will pack class data into the class. This could go as far as breaking your code when you update to the next compiler version.

Better use something like that:

class GLGXVECTOR4 {
private:
float vec[4];
public:
...
float x() { return vec[0]; }
...
operator float*() { return vec; }
...
}Then you can use it:

GLGLXVECTOR4 Loc;
glLightfv(GL_LIGHT0, GL_POSITION, Loc);This does not assume anything about the compiler dependant class layout, the speed is still the same, and as an additional bonus you don't need an explicit typecast ;)

Korval
05-16-2007, 10:41 AM
The C++ ANSI/ISO specification does not guarantee that this will work, though it almost always will.

Unless you suddenly decide that GLGXVECTOR3 (horrible name, btw) needs to have virtual functions.

In general, you should not rely on this. Indeed, it's better for you to have a specialized "glxLightPosition" function that takes this class natively. It could be inlined to the standard "glLightfv" class if you feel performance could be an issue.

greg2
05-16-2007, 11:55 AM
Korval, i don't want to write specialized "glxLightPosition" function because i want to be able to use my vector classes with any openGL function or non openGL function. (and it would be like rewritting a new 3D API, a thing i don't want). And even with "inline", i would have to do some conversion in the function which will not be better for performances.

i have used the overmind's suggestion to write this:


class GLGXVECTOR4{
private:
float vec[4];
public:
union{
float& x;
float& u;
};
union{
float& y;
float& v;
};
union{
float& z;
float& r;
};
union{
float& w;
float& q;
};

GLGXVECTOR4():x(vec[0]),y(vec[1]),z(vec[2]),w(vec[3]),u(vec[0]),v(vec[1]),r(vec[2]),q(vec[3])
{
x=0.0f;y=0.0f;z=0.0f;w=0.0f;
};

GLGXVECTOR4( float x , float y , float z, float w ):x(vec[0]),y(vec[1]),z(vec[2]),w(vec[3]),u(vec[0]),v(vec[1]),r(vec[2]),q(vec[3])
{
this->x = x; this->y = y; this->z = z;this->w = w;
}

operator float*()
{
return vec;
}

GLGXVECTOR4 operator= (GLGXVECTOR4 &B)
{
if (this == &B) return B;
this->x = B.x;
this->y = B.y;
this->z = B.z;
this->w = B.w;
return B;
}
~GLGXVECTOR4(){};

GLGXVECTOR4 operator+ (GLGXVECTOR4 &B);
GLGXVECTOR4 operator- (GLGXVECTOR4 &B);
float operator* (GLGXVECTOR4 &B);
GLGXVECTOR4 operator* (float factor);
GLGXVECTOR4 operator/ (float factor);
};It allow me to have direct access to the xyz variables ( Loc.x, Loc.y ,...), i can do sometings like v3 = v1 + v2 (where v1,v2,v3 are GLGXVECTOR4), and i can give the vector to the opengl function like glLightfv.

It seems to work.

Do you have other suggestions ? Is there something wrong in this class ?

Cyranose
05-16-2007, 03:06 PM
There is a safe way to do what you originally wanted, and that's to add a cast-to-float-pointer converter. Doing this always finds the right memory offset for your members and avoids the problems Korval mentioned.

Basically, just add to your class:


inline operator float*() { return &x; }
inline operator const float*() const { return (const float*)&x; }With that, you can call glVertex3fv(object) without even using the (float*) cast. Object is in this case not a pointer, but if it was, you would just dereference it.

Now you're not dependent on where in your class the XYZ floats wind up, but you could potentially be burned if XYZ are not contiguous (which only really happens in unusual alignment schemes, say of multiple sub-structures). The safest way to prevent problems is to declare these members as a union of two or more structures, see below.

This also works for non-float types just as well. One thing to watch, though, is that you should make a habit of putting the 'explicit' keyword on constructors that might accidentally invoke this auto-conversion inappropriately.

I'll give you my Vec3 template (minus the math functions) in case this helps:


template <class T> struct Vec3
{
typedef T ElemType;
static uint ElemGLID;
enum { ElemCount = 3 };

Vec3<T>() { x = y = z = 0; }
Vec3<T>(T x, T y, T z) : x(x), y(y), z(z) { }
Vec3<T>(const Vec3<T>&amp; u) : x(u.x), y(u.y), z(u.z) { }

explicit Vec3<T>(const T val) : x(val), y(val), z(val) { }
explicit Vec3<T>(const T* xyz) : x(xyz[0]), y(xyz[1]), z(xyz[2]) { }
explicit Vec3<T>(const Vec2<T>&amp; u) : x(u.x), y(u.y), z((T)1) { }
explicit Vec3<T>(const Vec4<T>&amp;);

inline void Set(T _x, T _y, T _z) { x = _x; y = _y; z = _z; }
inline Vec3<T>&amp; operator=(T val) { Set(val,val,val); return *this; }
inline Vec3<T>&amp; operator=(const Vec3<T>&amp; v) { Set(v.x,v.y,v.z); return *this; }
inline Vec3<T>&amp; operator=(const Vec4<T>&amp; v);

Bool operator==(const Vec3<T> & u) const
{
return (u.x == x &amp;&amp; u.y == y &amp;&amp; u.z == z) ? true : false;
}

typedef T* pointer;

operator pointer() { return &amp;x; }
operator const pointer() const { return (const pointer)&amp;x; }

union {
struct {
T x,y,z;
};
struct {
T s,t,u;
};
struct {
T r,g,b;
};
T elem[3]; // array access
};
};

greg2
05-17-2007, 02:28 AM
Thanks. I will see what i can do with all that.

Overmind
05-17-2007, 05:13 AM
Basically, just add to your class: ...That's equally dangerous. Ok, the offset to the first element is guaranteed to be correct, but the elements inside the struct are not guaranteed to be tightly packed. You're still making assumptions that may be correct or not, depending on the compiler, architecture, operating system and so on...

k_szczech
05-18-2007, 05:01 AM
I've just found an interesting article with this example:

struct A
{
char c;
int i;
}
struct B
{
char c;
public:
int i;
};They say that: "a compiler could move B::i before B::c, but A::c must precede A::i."

Full article here:
http://www.open-std.org/JTC1/sc22/wg21/docs/cwg_active.html

Another thing is that people rely on structure member order pretty much and I guess no one wants to make a compiler that will not be used by anyone. So this relation between member order in structure and real order in memory will be somewhat enforced by programmers upon compilers.

Just look at Windows API and DirectX - they rely on member order in structures. A great example would be vertex structure in Direct3D.

greg2
05-18-2007, 05:32 AM
I have looked at the D3DX header, and they seems to use a basic x,y,z declaration in the structure, so they probably just return the adresse of x in "operator float*()". They also use an union of "float _11,_12,..." and "float array[4][4]" for D3DXMATRIX. The memory is probably allocated in the same order.

But The DirectX SDK is written to be used on Windows and with Visual C++ only.

I want to create a library which could work with any operating system and compiler ( Windows, Haiku, Linux).

The use of references in initialization lists seems to be 100% safe but maybe not 100% optimized.

k_szczech
05-18-2007, 08:04 AM
Ok, let's make it official (I knew I read it somewhere :) ):

Nonstatic data members of a (non-union) class declared without an intervening access-specifier are allocated so that later members have higher addresses within a class object. This is from ISO/IEC 14882:1998.
Here is the link:
http://www.kuzbass.ru:8086/docs/isocpp/class.html

So if your compiler does not place structure/class member in proper order then report C++ standard violation to compiler developer :)

greg2
05-18-2007, 08:20 AM
I am not an expert in C++ and my english is not perfect. What does "non-union" mean" ?

Does your link mean that i can use something like that without any problem:


class GLGXVECTOR3{
public:
union{
struct {
float x,y,z;
}
struct {
float u,v,r;
}
};

GLGXVECTOR3();
GLGXVECTOR3( float x , float y , float z );
~GLGXVECTOR3(){};

inline operator float*() { return &amp;x; }
inline operator const float*() const { return (const float*)&amp;x; }

GLGXVECTOR3 operator+ (GLGXVECTOR3 &amp;B);
GLGXVECTOR3 operator- (GLGXVECTOR3 &amp;B);
float operator* (GLGXVECTOR3 &amp;B);
GLGXVECTOR3 operator* (float factor);
GLGXVECTOR3 operator/ (float factor);
};Or, do i have to remove the union ?

And higher adresse is not a synonym of consecutive ?

Stuart McDonald
05-18-2007, 08:47 AM
Originally posted by k_szczech:
So if your compiler does not place structure/class member in proper order then report C++ standard violation to compiler developer :) [/QB]But the problem is alignment, not order. So three ubit16s might be padded to four byte boundaries i.e 2 bytes of padding.

You could use offsetof to check at compile time though.
--Stuart.

k_szczech
05-18-2007, 08:50 AM
What does "non-union" mean" ?From specs:

class-key:
class
struct
union
So "non-union class" is a class or structure.


Come to think of it... I'm still not sure about this whole member ofset thing :D Look at this:

struct
{
int x, y;
};If structure is at address _adr_ then if x is at address (_adr_ + 100) and y is at address (_adr_ + 1024) then it's still compatible with what I quoted in my previous post.
Maybe there is some statement that members should be allocated at first possible location that is properly aligned? Need to read a bit further...

As for unions - same as above: we have to look for something in the specs about it. It may be that it's required that members are as tightly packed as possible without breaking alignment rules. That's what we want and that's what we have to look for.
Ready... set... go!

k_szczech
05-18-2007, 08:58 AM
Ok, more on unions.
Go to that link I posted and read Section 9.2 from -12- to -17-.
So that means first member is always at offset 0 and if you make union of two identical struct's they will overlap properly.

We also know that members will be in the same order as dclared.

The only thing remaining is to find out is it allowed for compiler to make more gap between members than required to achieve proper alignment.

Searching...

Overmind
05-18-2007, 10:09 AM
Maybe there is some statement that members should be allocated at first possible location that is properly aligned? Need to read a bit further...At least for non-uniforms, this is definitely not true, because there are some things that need to be stored at the beginning before other members (e.g. vtable or pointers to virtual base classes). This can be an arbitrary amount of data, especially with multiple inheritance.


The only thing remaining is to find out is it allowed for compiler to make more gap between members than required to achieve proper alignment.I'm pretty sure it is allowed to do this, but I don't know any reason why it should do it ;)
Perhaps some debugging constructs, you never know with these "undefined" aspects of specifications...

k_szczech
05-18-2007, 10:33 AM
I'm pretty sure it is allowed to do this, but I don't know any reason why it should do it
Looks like. Although there is a statement that the only gap between members is the one necessarry to achieve required alignment.
So if the gap is not necessary then it's not allowed?
So if I use byte-alignment I should have no gaps?
And if the gap is necessary then is it allowed for it to be bigger than necessary?

Looks like we almost have what we wanted but I found no place in specs that states that gaps are not allowed in other cases.
Well, but "almost have" means that we don't have anything actually :D

There is still the problem with padding structure to 16 bytes, but that can be solved with sizeof - you simply inform OpenGL that there is a gap between each vertex in the array you specified.

As for virtual functions - if you use proper type casting you'll get pointer to first member and if you take sizeof into account then you'll skip virtual function table of next object, too.
Anyway I think we can live without a virtual functions in a vec3, vec4 or mat4 classes, so let's just pretend they don't exist :)

k_szczech
05-18-2007, 01:54 PM
Ah, to hell with this. I'm gonna use structures and unions and don't care. It's gonna work anyway :)
If the day comes when compilers will become "different" I will rewrite some fragments of my code. Since I tend to use layered design for my applications it shouldn't be much work. It will probably come down to modifying wrapper for vertex array and loadin/saving data to file.


i don't want to write specialized "glxLightPosition" functionYou can always overload glLightfv and other functions.

PaladinOfKaos
05-18-2007, 03:53 PM
A compiler can add padding more-or-less wherever and however it wants, for a number of reasons:

1) Some processors (non-x86) can only address memory at certain multiples, so elements need to be padded

2) some processors are more efficient at accessing memory at certain multiples, so elements are padded

3) the compiler writers felt like it.

Most compilers have some way of overriding the default padding - for GCC, it would be:


class vec3
{
public:
union
{
struct __attribute__ ((__packed__))
{
float x, y, z;
}
struct __attribute__ ((__packed__))
{
float r, g, b;
}
struct __attribute__ ((__packed__))
{
float s, t, p;
}
};
...
}For MSVC, there's a #pragma that you need to use. I don't know about other compilers.

k_szczech
05-18-2007, 06:16 PM
Well, #pragma is used to tell the compiler what is the required alignment. So If I choose alignment=2 then compiler can theoretically still align to 4 bytes, because if something is aligned to 4 bytes then it's aligned to 2 bytes, too.

Perhaps __attribute__ ((__packed__)) enforces no padding but I don't know about any pragma that would do so.

Cyranose
05-18-2007, 10:48 PM
I step away for a day and miss all the fun...

You guys are making this way too complicated. In the example I posted, there is a union of structs:


union {
struct {
T x,y,z;
};
struct {
T s,t,u;
};
struct {
T r,g,b;
};
T elem[3]; // array accessIf you're concerned that x,y,z may not be tightly packed, just have the getter (the implicit type converter one I posted earlier) return the address of elem[0].

The array view of the union is guaranteed to be packed (the other elements are too in all normal cases) unless the following is true: you set your float alignment to 8 bytes. Floats will be correctly aligned and packed to 4-byte boundaries by default.

There are a million of lines of code out there that say "sizeof(array) = sizeof(elem) * length" or some variation. They wouldn't work if arrays were allowed to vary in packing.

k_szczech
05-19-2007, 03:57 AM
There are a million of lines of code out there that say "sizeof(array) = sizeof(elem) * length"Yeah, but that's for arrays and we are interested in structures.

Also, structures are guaranteed to have idenical allocation on first compatible elements when you put them in union, but I haven't found anything in the C/C++ specs that would say that struct elements will overlap array elements.

Yeah, sure it will work but not because C/C++ specs say it should but because compilers happen to overlap structures and arrays this way.

And that's the whole point of this discussion (besides initial question which has been answered).

So, if you found anything in the specs that guarantees expected overlapping of array and structure in the union then let us know. This would deal with our doubts. :)

Cyranose
05-19-2007, 11:37 PM
"Yeah, but that's for arrays and we are interested in structures."I think I understand the point of the discussion. And the C++ spec is frankly irrelevant, since you don't use a spec to compile your program...

The question is whether there are actual compilers out there that don't do the right/expected thing in this case. And I submit there aren't, at least not by their default behavior for the structures that were asked about. As long as we're talking about 4 floats defined as a simple struct or array, the layout will be tightly packed and 4-byte aligned by default.

It only gets funky when you start mixing elements that are smaller than 4 bytes with elements that need to be 4 or 8-byte aligned for ALUs or similar. That's when you might want the packing directives, which, btw, can really cause problems. You don't want to wind up with floats spanning those 4-byte boundaries, even if you're trying to tightly pack. You're much better off manually laying out your class to ensure floats and doubles are properly aligned in that case.

In general, it's best to stick to the default packing, which, as I said, works for unions of structs and arrays of float in every compiler I've ever tried.

k_szczech
05-20-2007, 03:24 AM
And the C++ spec is frankly irrelevant, since you don't use a spec to compile your programThats exactly whad I had in mind when I wrote:

Another thing is that people rely on structure member order pretty much and I guess no one wants to make a compiler that will not be used by anyone. So this relation between member order in structure and real order in memory will be somewhat enforced by programmers upon compilers.And later:

Ah, to hell with this. I'm gonna use structures and unions and don't care. It's gonna work anywaySo the question if it's gonna work or not has been more less answered. The only question remaining is if there is a guarantee this should work. Note that I used word "should" - compilers can sometimes break the specs.

I think my final conclusoin (for now) is that it will work but it's not enforced by specs, so we just have no guarantee.

So forget the specs and welcome to the real world (hehe, real = float). Cheers and thanks to everyone who contributed to this discussion.