\$\begingroup\$
\$\endgroup\$
3
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
user167941user167941
-
\$\begingroup\$ Any reason why you aren’t just using glm? glm does everything this class does and more. \$\endgroup\$Indiana Kernick– Indiana Kernick2018年10月14日 21:14:47 +00:00Commented 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\$user167941– user1679412018年10月14日 22:27:30 +00:00Commented 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\$Indiana Kernick– Indiana Kernick2018年10月14日 22:44:38 +00:00Commented Oct 14, 2018 at 22:44
1 Answer 1
\$\begingroup\$
\$\endgroup\$
1
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
, notstd::memcpy
. It's just as fast (if not faster), and doesn't havememcpy
'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 withx()
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
-
\$\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– user1679412018年10月15日 15:09:15 +00:00Commented Oct 15, 2018 at 15:09
lang-cpp