Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does this C++ setter/getter pattern break?

Using the GLSL syntax in C++

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;

The problem of swizzling

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.

Transparent getter and setter

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.

Limitations

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.

like image 644
sam hocevar Avatar asked Jan 26 '12 01:01

sam hocevar


2 Answers

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.

Correctness of the above code

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

Almost problems, but not quite

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.

UPDATE: An actual problem

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.

The proxy pattern

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.

like image 74
Blaisorblade Avatar answered Sep 27 '22 16:09

Blaisorblade


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

like image 32
sam hocevar Avatar answered Sep 27 '22 16:09

sam hocevar