Search code examples
c++vectormath

Is there a way to conditionally alias a member variable in a templated class or struct


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.


Solution

  • 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';
    }