10
\$\begingroup\$

I wrote a basic template for a vector of arbitrary dimension to be used in a 3D-engine. Most operators make use of C++ template meta programming and instances are implicitly convertible to glm types. It is also possible to build a matrix class on top of this. Suggestions in terms of optimizations, safety, readability etc. are welcome.

Here we go:

#ifndef Log2VECTOR_H
#define Log2VECTOR_H
#include <glm/glm.hpp>
#include <ostream>
namespace Log2
{
 template<unsigned Dim, typename T>
 class Vector
 {
 public:
 /**
 * Constructors
 */
 Vector() = default;
 Vector(const T& value)
 {
 for (unsigned i = 0; i < Dim; i++) {
 _data[i] = value;
 }
 }
 template<typename ...Args>
 Vector(Args... args) : _data{ args... }
 {
 }
 /**
 * Copy constructors
 */
 Vector(const Vector& other) = default;
 template<typename T1>
 Vector(const Vector<Dim, T1>& other)
 {
 for (unsigned i = 0; i < Dim; i++) {
 _data[i] = static_cast<T>(other[i]);
 }
 }
 Vector(const Vector<Dim - 1, T>& other, T scalar)
 {
 std::memcpy(_data, other.ptr(), sizeof other);
 _data[Dim - 1] = scalar;
 }
 /**
 * Concatenation
 */
 template<unsigned Dim1>
 Vector(const Vector<Dim1, T>& v1, const Vector<Dim - Dim1, T>& v2)
 {
 std::memcpy(_data, v1.ptr(), sizeof v1);
 std::memcpy(_data + Dim1, v2.ptr(), sizeof v2);
 }
 /**
 * Member access
 */
 inline auto & operator [] (unsigned index)
 {
 return _data[index];
 }
 inline const auto & operator [] (unsigned index) const
 {
 return _data[index];
 }
 inline const auto * ptr() const
 {
 return _data;
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 4, Vector<3, T>>::type xyz() const
 {
 return Vector<3, T>(_data[0], _data[1], _data[2]);
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 3, Vector<2, T>>::type xz() const
 {
 return Vector<2, T>(_data[0], _data[2]);
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 3, Vector<2, T>>::type xy() const
 {
 return Vector<2, T>(_data[0], _data[1]);
 }
 inline const auto& x() const
 {
 return _data[0];
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 2, T>::type const & y() const
 {
 return _data[1];
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 3, T>::type const & z() const
 {
 return _data[2];
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 4, T>::type const & w() const
 {
 return _data[3];
 }
 inline const auto& r() const
 {
 return _data[0];
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 2, T>::type const & g() const
 {
 return _data[1];
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 3, T>::type const & b() const
 {
 return _data[2];
 }
 template<unsigned D = Dim>
 inline typename std::enable_if<D >= 4, T>::type const & a() const
 {
 return _data[3];
 }
 template<unsigned index>
 inline const auto& at() const
 {
 static_assert(index < Dim, "Invalid index");
 return _data[index];
 }
 template<unsigned index>
 inline auto& at()
 {
 static_assert(index < Dim, "Invalid index");
 return _data[index];
 }
 /**
 * Vector/vector calculations
 */
 inline auto operator + (const Vector& other) const
 {
 Vector result;
 ComputeAdd<Dim - 1>::call(*this, other, result);
 return result;
 }
 inline auto operator - (const Vector& other) const
 {
 Vector result;
 ComputeSub<Dim - 1>::call(*this, other, result);
 return result;
 }
 inline auto operator * (const Vector& other) const
 {
 Vector result;
 ComputeMul<Dim - 1>::call(*this, other, result);
 return result;
 }
 inline auto operator / (const Vector& other) const
 {
 Vector result;
 ComputeDiv<Dim - 1>::call(*this, other, result);
 return result;
 }
 inline auto& operator += (const Vector& other)
 {
 ComputeAdd<Dim - 1>::call(*this, other, *this);
 return *this;
 }
 inline auto& operator -= (const Vector& other)
 {
 ComputeSub<Dim - 1>::call(*this, other, *this);
 return *this;
 }
 inline auto& operator *= (const Vector& other)
 {
 ComputeMul<Dim - 1>::call(*this, other, *this);
 return *this;
 }
 inline auto& operator /= (const Vector& other)
 {
 ComputeDiv<Dim - 1>::call(*this, other, *this);
 return *this;
 }
 /**
 * Comparison operators
 */
 inline auto operator == (const Vector& other) const
 {
 return ComputeEqual<Dim - 1>::call(*this, other);
 }
 inline auto operator != (const Vector& other) const
 {
 return ComputeInequal<Dim - 1>::call(*this, other);
 }
 inline auto operator < (const Vector& other) const
 {
 return ComputeLess<Dim - 1>::call(*this, other);
 }
 inline auto operator > (const Vector& other) const
 {
 return ComputeGreater<Dim - 1>::call(*this, other);
 }
 inline auto operator <= (const Vector& other) const
 {
 return ComputeLessOrEqual<Dim - 1>::call(*this, other);
 }
 inline auto operator >= (const Vector& other) const
 {
 return ComputeGreaterOrEqual<Dim - 1>::call(*this, other);
 }
 /**
 * Vector length
 */
 inline auto length() const
 {
 return std::sqrt(length2());
 }
 /**
 * Squared Vector length
 */
 inline auto length2() const
 {
 return dot(*this, *this);
 }
 /**
 * Conversion from/to glm
 */
 inline Vector(const glm::vec<Dim, T>& vec)
 {
 std::memcpy(_data, &vec[0], sizeof vec);
 }
 inline operator glm::vec<Dim, T>() const
 {
 glm::vec<Dim, T> vec;
 std::memcpy(&vec[0], ptr(), sizeof vec);
 return vec;
 }
 /**
 * Change in dimension
 */
 template<unsigned N>
 inline operator Vector<N, T>() const
 {
 Vector<N, T> ret;
 std::memcpy(&ret[0], ptr(), std::min(sizeof ret, sizeof *this));
 return ret;
 }
 /**
 * Debug output
 */
 friend std::ostream& operator << (std::ostream& os, const Vector& vec)
 {
 os << "[";
 for (unsigned i = 0; i < Dim; i++) {
 os << vec._data[i];
 if (i != Dim - 1) {
 os << " ";
 }
 }
 os << "]";
 return os;
 }
 private:
 T _data[Dim];
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeAdd
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector <D, Type>& result)
 {
 result[index] = a[index] + b[index];
 ComputeAdd<index - 1, D, Type>::call(a, b, result);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeAdd<0, D, Type>
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector<D, Type>& result)
 {
 result[0] = a[0] + b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeSub
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector <D, Type>& result)
 {
 result[index] = a[index] - b[index];
 ComputeSub<index - 1, D, Type>::call(a, b, result);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeSub<0, D, Type>
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector<D, Type>& result)
 {
 result[0] = a[0] - b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeMul
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector <D, Type>& result)
 {
 result[index] = a[index] * b[index];
 ComputeMul<index - 1, D, Type>::call(a, b, result);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeMul<0, D, Type>
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector<D, Type>& result)
 {
 result[0] = a[0] * b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeDiv
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector <D, Type>& result)
 {
 result[index] = a[index] / b[index];
 ComputeDiv<index - 1, D, Type>::call(a, b, result);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeDiv<0, D, Type>
 {
 static inline void call(const Vector<D, Type>& a, const Vector<D, Type>& b, Vector<D, Type>& result)
 {
 result[0] = a[0] / b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeEqual
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[index] == b[index] && ComputeEqual<index - 1, D, Type>::call(a, b);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeEqual<0, D, Type>
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[0] == b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeInequal
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[index] != b[index] || ComputeInequal<index - 1, D, Type>::call(a, b);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeInequal<0, D, Type>
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[0] != b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeLess
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[index] < b[index] && ComputeLess<index - 1, D, Type>::call(a, b);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeLess<0, D, Type>
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[0] < b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeGreater
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[index] > b[index] && ComputeGreater<index - 1, D, Type>::call(a, b);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeGreater<0, D, Type>
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[0] > b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeLessOrEqual
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[index] <= b[index] && ComputeLessOrEqual<index - 1, D, Type>::call(a, b);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeLessOrEqual<0, D, Type>
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[0] <= b[0];
 }
 };
 template<unsigned index, unsigned D = Dim, typename Type = T>
 struct ComputeGreaterOrEqual
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[index] >= b[index] && ComputeGreaterOrEqual<index - 1, D, Type>::call(a, b);
 }
 };
 template<unsigned D, typename Type>
 struct ComputeGreaterOrEqual<0, D, Type>
 {
 static inline auto call(const Vector<D, Type>& a, const Vector<D, Type>& b)
 {
 return a[0] >= b[0];
 }
 };
 };
 using Vec2f = Vector<2, float>;
 using Vec3f = Vector<3, float>;
 using Vec4f = Vector<4, float>;
 using Vec2u = Vector<2, unsigned>;
 using Vec3u = Vector<3, unsigned>;
 using Vec4u = Vector<4, unsigned>;
 using Vec2i = Vector<2, int>;
 using Vec3i = Vector<3, int>;
 using Vec4i = Vector<4, int>;
}
#endif

Edit: Definition of the dot product for code completeness:

 template<unsigned Dim, typename T>
 static inline T dot(const Vector<Dim, T>& a, const Vector<Dim, T>& b)
 {
 return ComputeDot<Dim, T, Dim - 1>::call(a, b);
 }
 template<unsigned Dim, typename T, unsigned index>
 struct ComputeDot
 {
 static inline T call(const Vector<Dim, T>& a, const Vector<Dim, T>& b)
 {
 return a[index] * b[index] + ComputeDot<Dim, T, index - 1>::call(a, b);
 }
 };
 template<unsigned Dim, typename T>
 struct ComputeDot<Dim, T, 0>
 {
 static inline T call(const Vector<Dim, T>& a, const Vector<Dim, T>& b)
 {
 return a[0] * b[0];
 }
 };
Toby Speight
87.2k14 gold badges104 silver badges322 bronze badges
asked Oct 14, 2018 at 16:29
\$\endgroup\$
3
  • \$\begingroup\$ Any reason why you aren’t just using glm? glm does everything this class does and more. \$\endgroup\$ Commented Oct 14, 2018 at 21:14
  • \$\begingroup\$ Sure. It's mainly practicing + I prefer writing my own source code instead of relying on third-party libs. \$\endgroup\$ Commented Oct 14, 2018 at 22:27
  • \$\begingroup\$ Fair enough. I used to have the same attitude but I found that if I used libraries, I could spend more time on the problem I’m trying to solve instead of implementation details. \$\endgroup\$ Commented Oct 14, 2018 at 22:44

1 Answer 1

11
\$\begingroup\$

A few issues:

  • Functions defined in the class in the header are inline by default, so there's no need to use the keyword everywhere.
  • Use std::copy, not std::memcpy. It's just as fast (if not faster), and doesn't have memcpy's trivially copyable requirement (currently there's no check to make sure the objects stored in the class are trivially copyable).
  • ComputeAdd etc. could be implemented with simple for loops.
  • Certain operators can be implemented in terms of other operators to save on code duplication (e.g. == / !=).
  • Returning by value with xyz() etc., and then returning a reference with x() etc. is potentially error prone. I'd suggest uniformly returning a copy.
  • It might be useful to have a non-const version of ptr().
  • The default constructor will not initialize values in the array. Is a (potential and minor) performance gain in certain circumstances really worth the massive, omnipresent risk?
  • The template parameter pack constructor will work for numbers of arguments smaller than the dimensions of the vector, and initialize any non-explicitly initialized values implicitly (i.e. to zero). This could be confusing.
  • Conversion operators should be explicit, to prevent unhappy accidents. One argument constructors should also be explicit.
  • A static size() member function to return the number of dimensions might be useful.
  • Iterator support might be useful.

  • Using std::array instead of a raw c-style array might fix some of the above or make implementing them (e.g. iterators) easier...
answered Oct 14, 2018 at 19:10
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the hints. I used std::array at first, but my program became slow in debug mode, so I switched to a raw C array. On the other hand, what's the point of running a renderer in debug mode xD. I guess for loop vs. recursion is a matter of taste, I just like the recursive definition better. \$\endgroup\$ Commented Oct 15, 2018 at 15:09

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.