user2921464
user2921464

Reputation: 77

Writing a generalized Vector Class in C++

I'm trying to implement a Vector class for use in my graphics projects. I'd like to template the vector by length and type, and I'd like to be able to use A.x, A.y, A.z, A.w to access the members of vector A (for example).

Here's my first attempt. I'm definitely not an expert in C++ templates! I was trying to implement a general Vector class with specialized versions for Vec2, Vec3, and Vec4. Each of the specialized classes would have a union allowing me to access the coordinates using their names. Unfortunately, I can't figure out how to do this without re-implementing every vector function for each of the specialized classes.

Keep in mind that I want to implement some functions that only apply to vectors of a certain length. For example, cross product only applies to vec3, but dot product (or operator*) applies to vectors of all length.

#include <cstdint>

using namespace std;

//*********************************************************************
// Vector implementation parameterized by type and size.
//*********************************************************************

template <typename T, size_t SIZE> 
class Vector {

    public:
        T data[SIZE];
        size_t size;

        Vector(T* arr);
};

template <typename T, size_t SIZE> Vector<T, SIZE>::Vector(T* arr) {
    size = SIZE;

    for(int i=0; i<size; i++) {
        data[i] = arr[i];
    }

}

//*********************************************************************
// Vec2 is a specialization of Vector with length 2.
//*********************************************************************

typedef Vector<float, 2> Vec2f;
typedef Vector<int, 2> Vec2d;

template <typename T>
class Vector <T, 2> {

    public:
        union {
            T data[2];
            struct {
                T x;
                T y;
            };
        };

        size_t size;

        Vector(T x, T y);
};

template <typename T> Vector<T, 2>::Vector(T x, T y) {
    data[0] = x; data[1] = y;
    size = 2;
}

//*********************************************************************
// Vec3 is a specialization of Vector with length 3.
//*********************************************************************

typedef Vector<float, 3> Vec3f;
typedef Vector<int, 3> Vec3d;

template <typename T>
class Vector <T, 3> {

    public:
        union {
            T data[3];
            struct {
                T x;
                T y;
                T z;
            };
        };

        size_t size;

        Vector(T x, T y, T z);
};

template <typename T> Vector<T, 3>::Vector(T x, T y, T z) {
    data[0] = x; data[1] = y; data[2] = z;
    size = 3;
}

//*********************************************************************
// Vec4 is a specialization of Vector with length 4.
//*********************************************************************

typedef Vector<float, 4> Vec4f;
typedef Vector<int, 4> Vec4d;

template <typename T>
class Vector <T, 4> {

    public:
        union {
            T data[4];
            struct {
                T x;
                T y;
                T z;
                T w;
            };
        };

        size_t size;

        Vector(T x, T y, T z, T w);
};

template <typename T> Vector<T, 4>::Vector(T x, T y, T z, T w) {
    data[0] = x; data[1] = y; data[2] = z; data[3] = w;
    size = 4;
}

Upvotes: 2

Views: 2397

Answers (1)

Casey
Casey

Reputation: 42554

The usual workaround to avoid repeatedly implementing identical features in multiple specializations is to inherit from a common base class, and implement those features in the base:

template <typename T>
struct VectorBase {
  // common stuff
};

template <typename T, std::size_t N>
struct Vector : VectorBase<T> {
  // ...
};

template <typename T>
struct Vector<T, 2> : VectorBase<T> {
  // ...
};

template <typename T>
struct Vector<T, 3> : VectorBase<T> {
  // ...
  friend Vector<T, 3> cross(const Vector<T, 3>&, const Vector<T, 3>&);
};

The next problem you will have is needing to access members in the derived class from the common base (e.g., get the value of x, or size()). You do that using the Curiously Recurring Template Pattern (CRTP):

template <typename T, typename CRTP>
struct VectorBase {
  CRTP& crtp() { return static_cast<CRTP&>(*this); }
  const CRTP& crtp() const { return static_cast<const CRTP&>(*this); }

  std::size_t size() const {
    return std::extent<decltype(CRTP::data)>::value;
  }

  void zero() {
    std::fill(std::begin(crtp().data), std::end(crtp().data), T());
  }

  using iterator = T*;
  using const_iterator = const T*;
  iterator begin() { return &crtp().data[0]; }
  iterator end() { return &crtp().data[0] + size(); }
  const_iterator begin() const { return &crtp().data[0]; }
  const_iterator end() const { return &crtp().data[0] + size(); }

  T& operator [] (std::size_t i) {
    return crtp().data[i];
  }
  const T& operator [] (std::size_t i) const {
    return crtp().data[i];
  }
};

template <typename T, std::size_t N>
struct Vector : VectorBase<T, Vector<T, N>> {
  union {
    T data[N];
    struct {
      T x, y, z, w;
    };
  };
};

template <typename T>
struct Vector<T, 2> : VectorBase<T, Vector<T, 2>> {
  union {
    T data[2];
    struct {
      T x, y;
    };
  };
};

template <typename T>
struct Vector<T, 3> : VectorBase<T, Vector<T, 3>> {
  union {
    T data[3];
    struct {
      T x, y, z;
    };
  };
};

template <typename T, typename U, std::size_t N>
auto operator * (const Vector<T, N>& a, const Vector<U, N>& b)
 -> Vector<decltype(a[0] * b[0]), N> {
  Vector<decltype(a[0] * b[0]), N> result;
  for (std::size_t i = 0; i < N; ++i) {
    result[i] = a[i] * b[i];
  }
  return result;
}

template <typename T, typename U, std::size_t N>
auto dot(const Vector<T, N>& a, const Vector<U, N>& b)
 -> decltype(a[0] * b[0]) {
  auto product = a * b;
  using V = decltype(product.x);
  return std::accumulate(std::begin(product), std::end(product), V(0));
}

** Sample Code at Coliru **

There two issues with undefined behavior to which the commenters refer are:

  1. The anonymous structs (e.g., struct { T x, y, z; };) are an GNU extension, and will thus likely only work with GCC and compatible compilers (clang).

  2. Reading from a union member other than the member last stored into is typically undefined behavior; this particular example is at least borderline given that every type involved is standard-layout and that all values read/written are of the same type. I'll let someone else do the exact language lawyering and simply state that the code will almost certainly perform as intended when using a recent compiler that supports the anonymous struct extension.

If either of those non-standard requirements bothers you, drop the structs and unions so that the array is the only data member. Then add functions for the symbolic names, e.g. T& x() { return data[0]; }, it's only slightly more cumbersome.

Upvotes: 5

Related Questions