2

Basically I am creating a vector math library and would like to more easily create variations of vector sizes using templates to determine the number of dimensions in the vector.

until now I've been not using templates, which has allowed me to use an nameless union to get members either by name or by array index without issue.

struct Vector3 {
    union {
        float v[3];
        struct { float x,y,z; }
    }
    // from here I can use either variable.v[0] or variable.x to represent the same data
    // vector math functions continue here..
}

I have no issue with using templates to increase the number of dimensions when it comes to the array, but being able to alias them via x,y,z etc is something I would like to keep, but seemingly cannot find a way to do so.

Ideally the solution would also have no impact to performance, but this may be asking for too much.

I've seen examples using conditional_t which can swap the type of variables, but I can't find a way to keep the exact same syntax that I currently have, since it doesn't appear to be possible to use either anonymous structs or unions, it doesn't doesn't remove the variable, just changes its type.

I've also tried using SFINAE to remove members from the union depending on the template, but it seems this only works with functions and not variables.

I also have a hard restriction on memory, so using reference variables isn't an option.

Currently my best option is to implement a function to access array members by reference eg float& x() { return v[0]; } but this doesn't keep the same syntax that I wanted, especially since any change in syntax results in having to change much of the code I've worked on using this library.

practically speaking, I only intend to use this letter aliasing for 2-4 dimensions, so a solution that allows me to hand code just the data multiple times would be acceptable.

final note, I know I could declare a macro to access the member, but to me this isn't an option since it would be a single letter macro, prone to breaking a lot of code.

Zami
  • 113
  • 5
  • 8
    Correct me if I'm wrong because my memory is a little foggy on this, but the type punning trick you're using with the anonymous union is an aliasing violation in C++ (but legal in C) – AndyG Mar 06 '23 at 14:13
  • 2
    As @AndyG said, type punning through a union like this in C++ has undefined behavior. It'll probably work but you have no guarantee, – NathanOliver Mar 06 '23 at 14:16
  • There's [this](https://stackoverflow.com/questions/65027336/how-to-update-multiple-fields-of-a-struct-simultaneously) and [what I wrote based on that](https://github.com/sinacam/swizzle). I don't exactly endorse using them in production though. – Passer By Mar 06 '23 at 14:33
  • 3
    `float& x() { return v[0]; }` is the C++ way to do this. Trying to reduce `.x()` to `.x` is fighting the language. You will never win such a fight. – BoP Mar 06 '23 at 14:36
  • @PasserBy From what I can tell, your approach requires you to redo all the functions for vec2 / vec3 / vec4, which is the exact scenario I'm attempting to avoid by conditionally applying a member alias – Zami Mar 06 '23 at 14:48
  • You'd do it `struct Vector3 { float vector[3]; float& x{ vector[0] }; float& y{ vector[1] }; float& z{ vector[2] }; };` Which, unfortunately, has a lot of caveats and (in my opinion) more pain than it is worth. That being said, use BoP's approach. – Eljay Mar 06 '23 at 15:00
  • @Eljay reference variables like this increase the memory footprint so I can't use them, as stated in the original post. – Zami Mar 06 '23 at 15:06
  • I realize you can't use the C++ way of doing what you want to do. I just wanted to make sure you were aware of the C++ way of doing it, and why BoP's suggestion is your answer. – Eljay Mar 06 '23 at 15:10
  • You can achieve something with some overloading of operators. Like v->*x but those always look more confusing compared to simple v.x() and might also perform worse. – Öö Tiib Mar 06 '23 at 15:11
  • You could take the problem the other way and have 3 members x, y and z, and add a subscript operator overload to access them as if they were an array (since they are probably stored consecutively in memory): [something like that](https://godbolt.org/z/cePznhhs3) But it is probably undefined behaviour too. Anyway, I would recommend using member functions, it's the better and safer way. – Fareanor Mar 06 '23 at 15:20

2 Answers2

3

If you want to make it portable, type-punning is out. I'd use getters:

enum idx : std::size_t { X, Y, Z, W };

template <class T, std::size_t N>
struct Vector {
    template <idx I>
    constexpr T& get() {
        static_assert(I < N, "That dimension does not exist");
        return m_data[I];
    }

    template <idx I>
    constexpr T get() const {
        return const_cast<Vector<T, N>*>(this)->get<I>();
    }

    constexpr T& operator[](std::size_t idx) { return m_data[idx]; }
    constexpr const T& operator[](std::size_t idx) const { return m_data[idx]; }

    T m_data[N]{};
};

Then with a Vector<double, 4> v4; you could access X, Y, Z and W like so:

std::cout << v4.get<X>() << ',' << v4.get<Y>() << ','
          << v4.get<Z>() << ',' << v4.get<W>() << '\n';

Demo


A second version could be to inherit from a number of base classes providing the correct accessors

template <class, std::size_t, std::size_t> struct accessors;

template <class T, std::size_t N>
struct accessors<T, N, 1> : std::array<T, N> {
    constexpr T& x() { return (*this)[X]; }
    constexpr const T& x() const { return (*this)[X]; }
};

template <class T, std::size_t N>
struct accessors<T, N, 2> : accessors<T, N, 1> {
    constexpr T& y() { return (*this)[Y]; }
    constexpr const T& y() const { return (*this)[Y]; }
};

template <class T, std::size_t N>
struct accessors<T, N, 3> : accessors<T, N, 2> {
    constexpr T& z() { return (*this)[Z]; }
    constexpr const T& z() const { return (*this)[Z]; }
};

template <class T, std::size_t N>
struct accessors<T, N, 4> : accessors<T, N, 3> {
    constexpr T& w() { return (*this)[W]; }
    constexpr const T& w() const { return (*this)[W]; }
};

Your Vector would then be ...

template <class T, std::size_t N>
struct Vector : accessors<T, N, N> {
    // ... operator+=, operator-= ...
};

.... and used like this:

int main() {
    constexpr Vector<double, 4> v1{1, 2, 3, 4}, v2{5, 6, 7, 8};
    constexpr auto v3 = v1 + v2;

    std::cout << v3.x() << ',' << v3.y() << ','
              << v3.z() << ',' << v3.w() << '\n';
}

Demo


A third alternative could be to use tagged subscript operator[] overloads.

struct tag_x {} X;
struct tag_y {} Y;
struct tag_z {} Z;
struct tag_w {} W;
using tags = std::tuple<tag_x, tag_y, tag_z, tag_w>;

template <class T, std::size_t N, std::size_t I>
struct tagged_subscr : tagged_subscr<T, N, I - 1> {
    using tagged_subscr<T, N, I - 1>::operator[];
    constexpr T& operator[](std::tuple_element_t<I - 1, tags>) {
        return (*this)[I - 1]; 
    }
    constexpr const T& operator[](std::tuple_element_t<I - 1, tags>) const {
        return (*this)[I - 1];
    }
};

template <class T, std::size_t N>
struct tagged_subscr<T, N, 1> : std::array<T, N> {
    using std::array<T, N>::operator[];
    constexpr T& operator[](tag_x) { return (*this)[0]; }
    constexpr const T& operator[](tag_x) const { return (*this)[0]; }
};
template <class T, std::size_t N>
struct Vector : tagged_subscr<T, N, N> { };

where the usage then becomes:

int main() {
    constexpr Vector<double, 4> v1{1, 2, 3, 4}, v2{5, 6, 7, 8};
    constexpr auto v3 = v1 + v2;

    std::cout << v3[X] << ',' << v3[Y] << ',' << v3[Z] << ',' << v3[W] << '\n';
}

Demo


I also have a hard restriction on memory, so using reference variables isn't an option

You don't have to store the references in the actual Vector, but you could create temporary overlays when you need them if you find using v4.x instead of any of the above (like v4[X]) a lot more convenient.

template <class, std::size_t> struct overlay;

template <class T>
struct overlay<T, 1> {
    template <class V> overlay(V& v) : x{v[X]} {}
    T& x;
};
template <class T>
struct overlay<T, 2> : overlay<T, 1> {
    template <class V> overlay(V& v) : overlay<T, 1>(v), y{v[Y]} {}
    T& y;
};
template <class T>
struct overlay<T, 3> : overlay<T, 2> {
    template <class V> overlay(V& v) : overlay<T, 2>(v), z{v[Z]} {}
    T& z;
};
template <class T>
struct overlay<T, 4> : overlay<T, 3> {
    template <class V> overlay(V& v) : overlay<T, 3>(v), w{v[W]} {}
    T& w;
};

// deduction guides:
template <template<class, std::size_t> class C, class T, std::size_t N>
overlay(C<T, N>&) -> overlay<T, N>;

template <template<class, std::size_t> class C, class T, std::size_t N>
overlay(const C<T, N>&) -> overlay<const T, N>;

Which could then be used like so:

int main() {
    constexpr Vector<double, 4> v1{1, 2, 3, 4}, v2{5, 6, 7, 8};
    constexpr auto v4 = v1 + v2;

    overlay o{v4};

    std::cout << o.x << ',' << o.y << ',' << o.z << ',' << o.w << '\n';
}

Demo

The overlay could even be used on std::arrays if you want to:

int main() {
    std::array<double, 4> a4{1, 2, 3, 4};

    overlay o{a4};

    std::cout << o.x << ',' << o.y << ',' << o.z << ',' << o.w << '\n';
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • to this point I think just write member function `x` `y` `z` is easier. – apple apple Mar 06 '23 at 15:49
  • @appleapple Yes, but OP wanted `z()` to not exist for a 2D `Vector`, and this way, `get()` won't compile on a 2D `Vector`. – Ted Lyngmo Mar 06 '23 at 15:49
  • 1
    as OP already state, it can be SFINAE/require out if it's a member function. – apple apple Mar 06 '23 at 15:51
  • @appleapple Yes, that's true. I think this will give a nicer error message though :) – Ted Lyngmo Mar 06 '23 at 15:52
  • 1
    you made me think maybe it'd be good to be able to associate message with require expression. – apple apple Mar 06 '23 at 15:54
  • @appleapple Could be, if it's possible to get a clearer error message than by using a `static_assert`, I'm all for it. – Ted Lyngmo Mar 06 '23 at 15:58
  • 1
    This doesn't perfectly fit my ideal solution (with current c++ standards probably isn't possible), but its likely as close as I will get while keeping standard compliant behaviour, so i'll go ahead and accept the answer. – Zami Mar 07 '23 at 03:21
  • @Zami I added two other versions: One that provides `x()` ... `w()` depending on how many dimensions your `Vector` has and another one using overlays to provide references `x` ... `w` depending on the number of dimensions. – Ted Lyngmo Mar 07 '23 at 17:37
1

I've managed to get a version that seems to solve the original issue.

struct _vec2 {
    union {
        float v[2];
        struct { float x, y; };
    };
};

struct _vec3 {
    union {
        float v[3];
        struct { float x, y, z; };
    };
};

template<unsigned int Dimensions = 3>
struct Vector : public std::conditional<Dimensions == 3, _vec3, _vec2>::type {
    // math functions here...
};

As others have pointed out though, type punning using unions the way I have is undefined behaviour in c++, so this isn't the perfect solution, but it does solve the original issue. I just need to add additional inheritance conditions for _vec4 and non-aliased versions beyond 4 dimensional array.

Zami
  • 113
  • 5
  • 1
    A program that invokes undefined behavior isn't a valid program at all; you can't rely on it to work. (even if it "seems to work" today it might break tomorrow) – Jeremy Friesner Mar 06 '23 at 15:47
  • @JeremyFriesner Which is precisely why I haven't accepted the answer, though truthfully I'm not sure the perfect solution does exist, at with current standards. – Zami Mar 06 '23 at 15:51
  • 1
    What is the upside of doing this instead of doing it in a way that will work with any compiler even when using `-Wall -Wextra -pedantic-errors`? Anyway, you may want to use specializations instead which makes the inheritance easier: [example](https://godbolt.org/z/eG9hY5Env) – Ted Lyngmo Mar 06 '23 at 16:18
  • @TedLyngmo The upside is to keep it consistent with existing codebase. Your absolutely right on the template specializations though, I'm not sure why I didn't think to use them. – Zami Mar 06 '23 at 16:22
  • It must be a huge amount of existing code if you are not willing to rewrite it into something that has defined behavior according to the standard though. – Ted Lyngmo Mar 07 '23 at 00:40