Designing math and linear algebra libraries in C++ often leads to a classic architectural challenge: how do you write a generic, fixed-size vector class like Vector<T, N> while still providing specialized accessors (such as .x, .y, .z) or dimension-specific functions (like cross products for 3D vectors) without rewriting core arithmetic operators?

The Core Problem

If you implement a generic Vector<T, N>, operations like operator+ return instances of Vector<T, N>. If you inherit from it to create a derived class Vector2<T> : public Vector<T, 2>, calling vec2_a + vec2_b will invoke the base class operator and slice the return value down to Vector<T, 2>, losing any derived-class identity or convenience methods.

Here are modern, idiomatic C++ solutions to solve this problem effectively.

Method 1: Template Specialization of a Storage / Data Base Class

The cleanest and most common approach used by modern linear algebra libraries (such as GLM or Eigen) is separating the data layout and dimension-specific accessors into a specialized base class, and letting the main Vector<T, N> inherit from it.

#include <cstddef>
#include <array>

// 1. Generic storage base for N dimensions
template <typename T, std::size_t N>
struct VectorStorage {
    std::array<T, N> values{};
};

// 2. Partial specialization for 2D vectors
template <typename T>
struct VectorStorage<T, 2> {
    union {
        std::array<T, 2> values{};
        struct { T x, y; };
    };
};

// 3. Partial specialization for 3D vectors
template <typename T>
struct VectorStorage<T, 3> {
    union {
        std::array<T, 3> values{};
        struct { T x, y, z; };
    };
};

// 4. Main Vector class that inherits from specialized storage
template <typename T, std::size_t N>
class Vector : public VectorStorage<T, N> {
public:
    using VectorStorage<T, N>::values;

    // Addition operator works seamlessly for any dimension
    Vector operator+(const Vector& rhs) const {
        Vector result;
        for (std::size_t i = 0; i < N; ++i) {
            result.values[i] = this->values[i] + rhs.values[i];
        }
        return result;
    }

    // Generic subscript operator
    T& operator[](std::size_t index) { return values[index]; }
    const T& operator[](std::size_t index) const { return values[index]; }
};

// Convenient type aliases
template <typename T>
using Vector2 = Vector<T, 2>;

template <typename T>
using Vector3 = Vector<T, 3>;

How to Use It:

int main() {
    Vector2<float> v1{ {1.0f, 2.0f} };
    Vector2<float> v2{ {3.0f, 4.0f} };

    Vector2<float> v3 = v1 + v2;

    // Directly access x and y properties
    float x = v3.x; // 4.0f
    float y = v3.y; // 6.0f
    
    return 0;
}

Method 2: Using C++20 Concepts and Constraints

If you want to add functions that only exist for specific dimensions (e.g., a cross product for 3D vectors, or a 2D rotation function), you can use C++20 requires clauses directly inside the class definition. This avoids the need for complex template inheritance entirely.

#include <cstddef>
#include <array>

template <typename T, std::size_t N>
class Vector {
public:
    std::array<T, N> values{};

    // Dedicated 2D getter/setters enabled only when N == 2
    T& x() requires (N >= 1) { return values[0]; }
    T& y() requires (N >= 2) { return values[1]; }
    T& z() requires (N >= 3) { return values[2]; }

    // Cross product only defined for 3D vectors
    Vector cross(const Vector& rhs) const requires (N == 3) {
        return Vector{{
            values[1] * rhs.values[2] - values[2] * rhs.values[1],
            values[2] * rhs.values[0] - values[0] * rhs.values[2],
            values[0] * rhs.values[1] - values[1] * rhs.values[0]
        }};
    }

    Vector operator+(const Vector& rhs) const {
        Vector result;
        for (std::size_t i = 0; i < N; ++i) {
            result.values[i] = values[i] + rhs.values[i];
        }
        return result;
    }
};

A Note on Safety and Cleanup

In your initial implementation, you had a runtime check comparing sizeof(values):

void failSafeVector(const Vector &rhs) {
    if (sizeof(this->values) != sizeof(rhs.values)) { ... }
}

Because both *this and rhs are of type Vector<T, N>, their size is strictly enforced at compile time. rhs can never have a different dimension or size than *this without causing a compiler error. Therefore, runtime dimension checking via sizeof or throwing std::out_of_range is completely unnecessary and can be removed for zero-overhead performance.

Conclusion

  • For data members (like .x, .y): Specialize an underlying base storage class (VectorStorage<T, N>) and inherit it in your main vector template.
  • For dimension-specific methods (like .cross()): Use C++20 requires clauses or std::enable_if to conditionally enable operations.
  • For arithmetic: Define operators inside the primary template so that they naturally return the right specialized vector type.