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];
    }
  };
\$\endgroup\$
  • \$\begingroup\$ Any reason why you aren’t just using glm? glm does everything this class does and more. \$\endgroup\$ – Kerndog73 Oct 14 '18 at 21:14
  • \$\begingroup\$ Sure. It's mainly practicing + I prefer writing my own source code instead of relying on third-party libs. \$\endgroup\$ – user167941 Oct 14 '18 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\$ – Kerndog73 Oct 14 '18 at 22:44
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...
\$\endgroup\$
  • \$\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\$ – user167941 Oct 15 '18 at 15:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy