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