How to Extend C++ Template Classes Based on Template Parameters
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++20requiresclauses orstd::enable_ifto conditionally enable operations. - For arithmetic: Define operators inside the primary template so that they naturally return the right specialized vector type.