When developing graphics engines, physics simulations, or game development tools, you frequently encounter the need for 2D, 3D, and 4D mathematical vectors (Vec2, Vec3, Vec4). Writing separate structs for each dimension quickly leads to hundreds of lines of duplicate code for standard mathematical operations like vector addition, dot products, and scalar multiplication.

However, generalizing these structs with C++ templates presents a classic design challenge: how do you allow uniform array-style indexing (e.g., v[0]) while maintaining intuitive coordinate access (e.g., v.x, v.y, v.z)?

The Problem with Anonymous Structs and Unions

The common C-style approach uses anonymous structs inside an anonymous union:

union {    struct { double x, y, z; };    double components[3];};

While supported as an extension by GCC, Clang, and MSVC, anonymous structs inside unions are not standard C++. Furthermore, trying to template this directly fails because the number of named variables (x, y, z, w) depends on the compile-time dimension parameter N.

Solution 1: Base Class Specialization (Preserving .x, .y Syntax)

If you want direct member access like v.x and v.y without sacrificing standard compliance or code reuse, the cleanest technique is to separate the storage from the vector operations using class template specialization.

Step 1: Define the Storage Base

#include <cstddef>#include <array>template <typename T, std::size_t N>struct VectorStorage {    T data[N];};template <typename T>struct VectorStorage<T, 2> {    union {        T data[2];        struct { T x, y; };    };};template <typename T>struct VectorStorage<T, 3> {    union {        T data[3];        struct { T x, y, z; };    };};

Step 2: Derive the General Vector Template

Next, define the unified Vector class that inherits from VectorStorage and implements generic mathematical operations using modern C++ variadic templates:

template <typename T, std::size_t N>struct Vector : public VectorStorage<T, N> {    // Default constructor    Vector() = default;    // Variadic constructor matching the exact dimension N    template <typename... Args, typename = std::enable_if_t<sizeof...(Args) == N>>    constexpr Vector(Args... args) : VectorStorage<T, N>{{ static_cast<T>(args)... }} {}    // Array subscript operators    constexpr T& operator[](std::size_t index) { return this->data[index]; }    constexpr const T& operator[](std::size_t index) const { return this->data[index]; }    // Component-wise addition    Vector& operator+=(const Vector& rhs) {        for (std::size_t i = 0; i < N; ++i) {            this->data[i] += rhs.data[i];        }        return *this;    }};template <typename T, std::size_t N>Vector<T, N> operator+(Vector<T, N> lhs, const Vector<T, N>& rhs) {    lhs += rhs;    return lhs;}

Step 3: Define Convenient Type Aliases

using Vec2d = Vector<double, 2>;using Vec3d = Vector<double, 3>;using Vec3f = Vector<float, 3>;int main() {    Vec2d a(1.0, 2.0);    Vec2d b(3.0, 4.0);    Vec2d c = a + b;    // Access via name or array index    double valX = c.x;       // 4.0    double valY = c[1];      // 6.0    Vec3d v3(1.0, 2.0, 3.0);    v3.z = 10.0;    return 0;}

Solution 2: Modern C++20 Accessors (100% Standard-Compliant)

If you prefer strictly compliant standard C++ that avoids union punning entirely, the modern best practice is using std::array and C++20 requires clauses for accessor methods:

#include <array>#include <cstddef>template <typename T, std::size_t N>struct Vector {    std::array<T, N> data{};    template <typename... Args>    requires (sizeof...(Args) == N)    constexpr Vector(Args... args) : data{ static_cast<T>(args)... } {}    constexpr T& operator[](std::size_t i) { return data[i]; }    constexpr const T& operator[](std::size_t i) const { return data[i]; }    // Member accessors constrained by dimension size    constexpr T& x() requires (N >= 1) { return data[0]; }    constexpr const T& x() const requires (N >= 1) { return data[0]; }    constexpr T& y() requires (N >= 2) { return data[1]; }    constexpr const T& y() const requires (N >= 2) { return data[1]; }    constexpr T& z() requires (N >= 3) { return data[2]; }    constexpr const T& z() const requires (N >= 3) { return data[2]; }};

Handling Constructor Ambiguities

In the original question, Vec3 had a default argument for z (e.g., z = 0). When using templates, relying on default arguments can cause constructor ambiguities with lower-dimension vectors. Instead, provide explicit converting constructors or dedicated factory functions:

// Explicit promotion: Create Vec3 from Vec2 and an optional z-valuetemplate <typename T, std::size_t N>struct Vector : public VectorStorage<T, N> {    // ... previous methods ...    template <std::size_t M = N, typename = std::enable_if_t<M == 3>>    Vector(const Vector<T, 2>& v2, T z = T(0)) {        this->data[0] = v2[0];        this->data[1] = v2[1];        this->data[2] = z;    }};

Summary

  • Base Struct Specialization gives you zero-overhead, familiar syntax like v.x, v.y, and v.z while unifying vector logic in a single template.
  • C++20 Concepts and Accessor Methods (v.x(), v.y()) provide maximum standard compliance and eliminate undefined behavior risks associated with union type-punning.
  • Variadic Constructors constrained by sizeof...(Args) == N ensure type safety while eliminating the need to write separate constructor overloads for every dimension.