Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

convenient Vector3f class

Tags:

c++

c++20

Sometimes there is a need to have a Vector3f class, which has x, y and z members, and can be indexed as a float[3] array at the same time (there are several questions here at SO already about this).

Something like:

struct Vector3f {
    float data[3];
    float &x = data[0];
    float &y = data[1];
    float &z = data[2];
};

With this, we can write this:

Vector3f v;
v.x = 2.0f;
v.y = 3.0f;
v.z = 4.0f;
glVertex3fv(v.data);

But this implementation is bad, because references take space in the struct (which is quite unfortunate. I don't see any reason why references cannot be removed in this particular case, maybe it is missed optimization from the compiler's part).

But, with [[no_unique_address]] I had this idea:

#include <new>

template <int INDEX>
class Vector3fProperty {
    public:
        operator float() const {
            return propertyValue();
        }
        float &operator=(float value) {
            float &v = propertyValue();
            v = value;
            return v;
        }
    private:
        float &propertyValue() {
            return std::launder(reinterpret_cast<float*>(this))[INDEX];
        }
        float propertyValue() const {
            return std::launder(reinterpret_cast<const float*>(this))[INDEX];
        }
};

struct Vector3f {
    [[no_unique_address]]
    Vector3fProperty<0> x;
    [[no_unique_address]]
    Vector3fProperty<1> y;
    [[no_unique_address]]
    Vector3fProperty<2> z;

    float data[3];
};

static_assert(sizeof(Vector3f)==12);

So, basically, I have properties in the struct, which handles the access to x, y and z. These properties should not take space, as they are empty, and have the attribute of [[no_unique_address]]

What do you think about this approach? Does it have UB?


Note, this question is about a class, for which all these are possible:

Vector3f v;
v.x = 1;
float tmp = v.x;
float *c = v.<something>; // there, c points to a float[3] array
like image 701
geza Avatar asked Feb 03 '23 17:02

geza


2 Answers

If this is going to live in a header, and you have some confidence in your compiler's optimizing capabilities, you can probably stick to a plain-old operator[]() overload and expect the compiler to be smart enough to elide the call and return the element that you want. E.g.:

class Vec3f {
public:
    float x;
    float y;
    float z;

    float &operator[](int i) {
        if(i == 0) {
            return x;
        }
        if(i == 1) {
            return y;
        }
        if(i == 2) {
            return z;
        }
    }
};

I tossed this into Compiler Explorer (https://godbolt.org/z/0X4FPL), which showed clang optimizing the operator[] call away at -O2, and GCC at -O3. Less exciting than your approach, but simple and should work under most circumstances.

like image 114
youngmit Avatar answered Feb 06 '23 09:02

youngmit


But this implementation is bad, because references take space in the struct (which is quite unfortunate. I don't see any reason why references cannot be removed in this particular case, maybe it is missed optimization from the compiler's part).

This looks like a complicated issue. Standard-layout classes have to be compatible between each other. And so compilers are not allowed to eliminate any member, regardless of how they are defined. For non standard-layout? Who knows. For more info read this: Do the C++ standards guarantee that unused private fields will influence sizeof?

From my experience compilers never remove class members, even if they are "unused" (e.g. formally sizeof does use them).

Does it have UB?

I think this is UB. First of all [[no_unique_address]] only means that the member need not have a unique address, not that it must not have a unique address. Secondly it is not clear where your data member starts. Again, compilers are free to use or not paddings of previous [[no_unique_address]] class members. Meaning your accessors may access incorrect piece of memory.

Another problem is that you want to access "outer" memory from the "inner" class. AFAIK such thing is also UB in C++.

What do you think about this approach?

Assuming it is correct (which is not) I still don't like it. You want getters/setters but C++ does not support this feature. So instead of doing those weird, complicated constructs (imagine other people maintaining this code) how about simply do

struct Vector3f {
    float data[3];
    float x() {
        return data[0];
    }
    void x(float value) {
        data[0] = value;
    }
    ...
};

You say this code is ugly. Maybe it is. But it is simple, easy to read and maintain. There's no UB, it does not depend on potential hacks with unions, and does exactly what you want, except for beauty requirement. :)

like image 33
freakish Avatar answered Feb 06 '23 08:02

freakish