2

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;
}
user2921464
  • 77
  • 2
  • 6

1 Answers1

5

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.

Community
  • 1
  • 1
Casey
  • 41,449
  • 7
  • 95
  • 125
  • Hey! Thanks for the help! I'm definitely still learning. This makes sense, but I'm still not sure how to access vec.data[0] as vec.x Should I give up on that and use vec.data[0] for everything? – user2921464 Jan 10 '14 at 01:10
  • Oh wait. I think you're suggesting I remove the data array and just have a field x in the base case, a field y in the length 2 case, a field z in the length 3 case, and a w in the length 4 case. This will work for me, but I'm curious if there's a way to do all this and still have a vector of arbitrary length backed by the data array. Does that make any sense? – user2921464 Jan 10 '14 at 01:13
  • 1
    @user2921464 I was not suggesting that, I simply elided the detail from the structs that was the same is in the OP. I was trying to focus on the use of the common base class + CRTP technique but apparently lost too much detail for clarity. I'll pull the heavy bits of the code on Coliru and post it all inline here, and throw in a dot product implementation just for an operator demo. – Casey Jan 10 '14 at 02:28
  • I think I'm starting to understand. Thanks for the extremely detailed explanation! – user2921464 Jan 10 '14 at 04:15
  • I'm having issues compiling this code. I used g++ test.cpp -std=gnu++0x These are the errors I can't seem to shake. Any ideas? http://pastebin.com/n930ES3K And here's my code. http://pastebin.com/czzvWGsX – user2921464 Jan 10 '14 at 05:00
  • 1
    @user2921464 Line 18 is the first occurrence of an alias declaration (`using iterator = T*;`). Type aliases were first supported in g++ 4.7, so I conjecture you are using an earlier version. ([Deja vu, just answered this an hour ago.](http://stackoverflow.com/a/21036149/923854)) Replaced them with the equivalent typedefs `typedef T* iterator; typedef const T* const_iterator`...`typedef decltype(product.x) V;`. – Casey Jan 10 '14 at 05:22
  • Thanks so much! I finally got it working. Now I'll see if I can do matrices in a similar way. – user2921464 Jan 10 '14 at 06:01