I wrote custom vector classes such as vec2
, vec3
etc. that mimic the GLSL types and look roughly like this:
struct vec3
{
inline vec3(float x, float y, float z)
: x(x), y(y), z(z) {}
union { float x, r, s; };
union { float y, g, t; };
union { float z, b, p; };
};
Operations on vectors are implemented this way:
inline vec3 operator +(vec3 a, vec3 b)
{
return vec3(a.x + b.x, a.y + b.y, a.z + b.z);
}
This allows me to create vectors and access their components using a GLSL-like syntax and perform operations on them almost as if they were numeric types. The unions allow me to refer to the first coordinate indifferently as x
or as r
, as is the case in GLSL. For instance:
vec3 point = vec3(1.f, 2.f, 3.f);
vec3 other = point + point;
point.x = other.b;
But GLSL also allows swizzled access, even with holes between components. For instance p.yx
behaves like a vec2
with p
’s x
and y
swapped. When no component is repeated, it is also an lvalue. Some examples:
other = point.xyy; /* Note: xyy, not xyz */
other.xz = point.xz;
point.xy = other.xx + vec2(1.0f, 2.0f);
Now this could be done using standard getters and setters such as vec2 xy()
and void xy(vec2 val)
. This is what the GLM library does.
However, I devised this pattern that lets me do exactly the same in C++. Since everything is a POD-struct, I can add more unions:
template<int I, int J> struct MagicVec2
{
friend struct vec2;
inline vec2 operator =(vec2 that);
private:
float ptr[1 + (I > J ? I : J)];
};
template<int I, int J>
inline vec2 MagicVec2<I, J>::operator =(vec2 that)
{
ptr[I] = that.x; ptr[J] = that.y;
return *this;
}
And eg. the vec3
class becomes (I simplified things a bit, for instance nothing prevents xx
from being used as an lvalue here):
struct vec3
{
inline vec3(float x, float y, float z)
: x(x), y(y), z(z) {}
template<int I, int J, int K>
inline vec3(MagicVec3<I, J, K> const &v)
: x(v.ptr[I]), y(v.ptr[J]), z(v.ptr[K]) {}
union
{
struct { float x, y, z; };
struct { float r, g, b; };
struct { float s, t, p; };
MagicVec2<0,0> xx, rr, ss;
MagicVec2<0,1> xy, rg, st;
MagicVec2<0,2> xz, rb, sp;
MagicVec2<1,0> yx, gr, ts;
MagicVec2<1,1> yy, gg, tt;
MagicVec2<1,2> yz, gb, tp;
MagicVec2<2,0> zx, br, ps;
MagicVec2<2,1> zy, bg, pt;
MagicVec2<2,2> zz, bb, pp;
/* Also MagicVec3 and MagicVec4, of course */
};
};
Basically: I use a union to mix the vector’s floating-point components with a magic object which is not really a vec2
but can be cast implicitly to a vec2
(because there’s a vec2
constructor allowing it), and can be assigned a vec2
(because of its overloaded assignment operator).
I am very satisfied with the result. The GLSL code above works and I believe I get decent type safety. And I can #include
a GLSL shader in my C++ code.
Of course there are limitations. I know of the following ones:
sizeof(point.xz)
will be 3*sizeof(float)
instead of the expected 2*sizeof(float)
. This is by design and I do not know whether this could be problematic.&foo.xz
cannot be used as a vec2*
. This should be OK because I only ever pass these objects by value.So my question is: what may I have overlooked that will make my life difficult with this pattern? Also, I have not found this pattern anywhere else yet, so if anyone knows its name I am interested.
Note: I wish to stick to C++98, but I do rely on the compiler allowing type-punning through unions. My reason for not wanting C++11 yet is the lack of compiler support on several of my target platforms; all the compilers that are of interest to me support type punning, though.
In short: I think that it is difficult to make sure that this pattern works - that's why you are asking. Moreover, this pattern could be replaced by a standard proxy pattern, for which correctness is easier to guarantee. I have to admit though that the storage overhead of a proxy-based solution is a problem when the proxies are created statically.
This is code where there is no obvious bug; but paraphrasing C. A. R. Hoare, this is not code where there is obviously no bug. Moreover, how hard is it to convince oneself that there is no bug?
I do not see any reason why the pattern would not work - but it is not so easy to prove (even informally) that it will work. In fact, trying doing a proof could fail and point out to some problems.
To be safe, I would disable all implicitly-generated constructors/assignment operators for MagicVecN
classes, just to avoid considering all the associated complications (see subsection below); however doing that is forbidden, because for union members one cannot override the implicitly defined copy assignment operator, as explained by the standard draft I have and by GCC's error message:
member ‘MagicVec2<0, 0> vec3::<anonymous union>::xx’ with copy assignment operator not allowed in union
In the attached gist, I instead provide an implementation manually to be safe.
Note that MagicVec2's assignment operator should accepts its parameter by const reference (see example below, where this works); implicit conversions still happen (the const reference will point to the created temporary; this would not work without the const qualifier).
I thought a found a bug (which I didn't), but it is still somewhat interesting to consider - just to see how many cases must be covered to rule out potential bugs. Would p.xz = p.zx
produce the correct results? I thought that MagicVec2
's implicit assignment operator would be invoked, leading to incorrect results; in fact, it isn't (I believe) because I
and J
are different and part of the type. What when the type is the same?
p.xx = q.rr
is safe, but p.xx = p.rr
is tricky (even though it might be stupid, but it should still not corrupt memory): is the implicitly-generated assignment operator memcpy
-based? The answer seems to be no, but if yes, this would be a memcpy
between overlapping memory intervals, which is undefined behavior.
As noticed by the OP, the default copy assignment operator is also invoked for the expression p.xz = q.xz
; in that case, it will in fact also copy the .y
member. As mentioned above, the copy assignment operator cannot be disabled or modified for datatypes which are part of an union.
Moreover, I believe that there is a much simpler solution, namely the proxy pattern (which you are partially using). MagicVecX
should contain a pointer to the containing class instead of ptr
; this way you need no trick using unions.
template<int I, int J> struct MagicVec2
{
friend struct vec2;
inline MagicVec2(vec2* _this): ptr(_this) {}
inline vec2 operator=(const vec2& that);
private:
float *ptr;
};
I tested this by compiling (but not linking) this code, which sketches the proposed solution: https://gist.github.com/1775054. Note that the code is not complete nor tested - one should also override the copy constructor of MagicVecX
.
Okay, I have found one problem already, though not directly with the code above. If vec3
is somehow made a template class in order to support eg. int
in addition to float
, and the +
operator becomes:
template<typename T>
inline vec3<T> operator +(vec3<T> a, vec3<T> b)
{
return vec3<T>(a.x + b.x, a.y + b.y, a.z + b.z);
}
Then this code will not work:
vec3<float> a, b, c;
...
c = a.xyz + b;
The reason is that figuring the arguments for +
will require both template argument deduction (T = float
) and an implicit conversion (from MagicVec3<T,0,1,2>
to vec3<T>
, which is not allowed.
There is however a solution acceptable for me: write all possible explicit operators.
inline vec3<int> operator +(vec3<int> a, vec3<int> b)
{
return vec3<int>(a.x + b.x, a.y + b.y, a.z + b.z);
}
inline vec3<float> operator +(vec3<float> a, vec3<float> b)
{
return vec3<float>(a.x + b.x, a.y + b.y, a.z + b.z);
}
This will also let me define rules for implicit promotion, for instance I can decide that vec3<float> + vec3<int>
is legal and will return a vec3<float>
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With