convenient Vector3f class

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.


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


GLM implements this kind of functionality using anonymous structs inside an anonymous union

I can't personally guarantee that this is standard-compliant, but most major compilers (MSVC, GCC, Clang) will support this idiom:

struct Vector3f {
    union {
        struct {
            float x, y, z;
        };
        struct {
            float data[3];
        };
    };
    Vector3f() : Vector3f(0,0,0) {}
    Vector3f(float x, float y, float z) : x(x), y(y), z(z) {}
};

int main() {
    Vector3f vec;
    vec.x = 14.5;
    std::cout << vec.data[0] << std::endl; //Should print 14.5
    vec.y = -22.345;
    std::cout << vec.data[1] << std::endl; //Should print -22.345
    std::cout << sizeof(vec) << std::endl; //On most platforms will print 12
}

The non-standard behavior is in the anonymous struct used to group the letters together, which GCC will issue a warning about. As far as I know, the union itself should be valid, because the datatypes are all identical, but you should still check with your compiler documentation if you're unsure whether this is valid or not.

As an added convenience, we can also overload the brackets operator to shorten our syntax a little:

struct Vector3f {
    /*...*/
    float& operator[](size_t index) {return data[index];}
    float operator[](size_t index) const {return data[index];}
};



int main() {
    Vector3f vec;
    vec.x = 14.5;
    std::cout << vec[0] << std::endl; //Should print 14.5
    vec.y = -22.345;
    std::cout << vec[1] << std::endl; //Should print -22.345
    std::cout << sizeof(vec) << std::endl; //On most platforms will print 12
}

Just for clarity, accessing inactive members in the way I am is valid according to the C++ standard, because those members share a "common subsequence":

If two union members are standard-layout types, it's well-defined to examine their common subsequence on any compiler.

CPP Reference: Union Declaration

Because x and data[0] are

  • Both floats,
  • Both occupy the same memory,
  • Are both standard Layout types as the standard defines them,

It's perfectly valid to access one or the other regardless of which is currently active.

Tags:

C++

C++20