I am currently playing a bit around. While the current compiler starts to finally release the c++17
standard implementations, I am trying to create a mathematical vector class, with variable dimensions, which are stored in an std::array
. This dimensions can be specified via template parameter.
The other requirement (beside the dynamic dimensions) is the usage of constexpr
. I want to be able to use this in compile time expressions; this is the reason why I avoid the std algorithms
. They are unluckily not declared constexpr
, which is a shame, imo...
But ok, either way; here is my implementation.
Some words before you start reading:
I will implement all vector algorithms (like length, normalize, etc.) as free functions. I don't want to bloat my class with that.
Second, I like getter
and setter
. I know, I could simply return a reference to my internal values, but I really don't like that. That's simply a matter of taste.
Last but not least, the free operators like +, -, etc are implemented in the inherited structs; That are just some simple helper classes, thus I think it isn't necessary to post them here.
#include <array>
#include <cassert>
#include <type_traits>
#include "operators/compare.hpp"
#include "operators/arithmetic.hpp"
/*!
* \class Vector
*
* \brief A dynamic dimensional vector class. It provides access via the operator [], and for common dimensions,
* getters and setters (e.g. getX(), setX()).
*
* It provides (inherited) arithmetic functions for both, Vector<T, DIM> and T. Look at the specific struct documentations
* to get more details.
*
* \tparam T Generic type parameter. Must be an arithmetic type.
* \tparam DIM Number of dimensions.
*/
template<class T, std::size_t DIM>
class Vector :
operators::Equal<Vector<T, DIM>>,
operators::Plus<Vector<T, DIM>>,
operators::Minus<Vector<T, DIM>>,
operators::Arithmetic2<Vector<T, DIM>, T>
{
private:
static_assert(DIM > 0, "DIM must be greater than 0.");
static_assert(std::is_arithmetic_v<T>, "T must be an arithmetic type.");
public:
enum class CommonDimensions
{
x = 0,
y,
z
};
/*!
* \brief default constructor
*/
constexpr Vector() noexcept = default;
/*!
* \brief copy constructor
*/
constexpr Vector(const Vector&) noexcept = default;
/*!
* \brief move constructor
*/
constexpr Vector(Vector&&) noexcept = default;
/*!
* \brief copy-assignment operator.
*
* \return A reference of this object.
*/
constexpr Vector& operator =(const Vector&) noexcept = default;
/*!
* \brief move-assignment operator.
*
* \return A reference of this object.
*/
constexpr Vector& operator =(Vector&&) noexcept = default;
/*!
* \brief template constructor
*
* \details Variadic template constructor for direct access on the underlaying std::array.
* \remark The enable_if is used to SFINAE this constructor for copy or move purposes.
*
* \tparam Args Type of the arguments.
* \param _args Arguments for initialising the underlaying std::array.
*/
template <class... Args, typename = std::enable_if_t<!(sizeof...(Args) == 1 && std::is_same_v<std::common_type_t<Args...>, Vector<T, DIM>>)>>
constexpr explicit Vector(Args&&... _args) noexcept :
m_Values{ std::forward<Args>(_args)... }
{
}
/*!
* \brief Gets the value at the passed index.
*
* \remark This function doesn't perform any out-of-bound check, thus it is undefined
* behavior to pass an invalid index [0, DIM).
* \tparam Index Type of the index.
* \param _index Index of value.
*
* \return The result of the operation.
*/
template <class Index>
constexpr T operator [](Index _index) const noexcept
{
std::size_t index = 0;
if constexpr (std::is_same_v<Index, CommonDimensions>)
index = static_cast<std::size_t>(_index);
else
index = _index;
assert(0 <= index && index < DIM);
return m_Values[index];
}
/*!
* \brief Sets the value at the passed index.
*
* \remark This function doesn't perform any out-of-bound check, thus it is undefined
* behavior to pass an invalid index [0, DIM).
* \tparam Index Type of the index.
* \tparam U must be implicit convertible to T.
* \param _index Index of value.
* \param _val The value.
*/
template <class Index, typename U>
constexpr void set(Index _index, U&& _val) noexcept
{
std::size_t index = 0;
if constexpr (std::is_same_v<Index, CommonDimensions>)
index = static_cast<std::size_t>(_index);
else
index = _index;
assert(0 <= index && index < DIM);
m_Values[index] = std::forward<U>(_val);
}
/*!
* \brief Gets the dimensions
*
* \return The dimensions.
*/
constexpr std::size_t getDimensions() const noexcept
{
return DIM;
}
/*!
* \brief gets X value
*
* \tparam Index This is a little trick to make the std::enable_if a dependent name. Do not pass any other type than the default one.
* \return Returns value of X.
*/
template <typename Index = std::size_t, typename = std::enable_if_t<std::greater<Index>()(DIM, static_cast<Index>(CommonDimensions::x))>>
constexpr decltype(auto) getX() const noexcept
{
return (*this)[CommonDimensions::x];
}
/*!
* \brief sets X value
*
* \tparam U must be implicit convertible to T
* \tparam Index This is a little trick to make the std::enable_if a dependent name. Do not pass any other type than the default one.
* \param _value The value.
*/
template <typename U, typename Index = std::size_t, typename = std::enable_if_t<std::greater<Index>()(DIM, static_cast<Index>(CommonDimensions::x))>>
constexpr void setX(U&& _value) noexcept
{
set(CommonDimensions::x, std::forward<U>(_value));
}
/*!
* \brief gets Y value
*
* \remark This function will be available only, if this Vector has 2 or more dimensions.
* \tparam Index This is a little trick to make the std::enable_if a dependent name. Do not pass any other type than the default one.
* \return Returns value of Y.
*/
template <typename Index = std::size_t, typename = std::enable_if_t<std::greater<Index>()(DIM, static_cast<Index>(CommonDimensions::y))>>
constexpr decltype(auto) getY() const noexcept
{
return (*this)[CommonDimensions::y];
}
/*!
* \brief sets Y value
*
* \remark This function will be available only, if this Vector has 2 or more dimensions.
*
* \tparam U must be implicit convertible to T
* \tparam Index This is a little trick to make the std::enable_if a dependent name. Do not pass any other type than the default one.
* \param _value The value.
*/
template <typename U, typename Index = std::size_t, typename = std::enable_if_t<std::greater<Index>()(DIM, static_cast<Index>(CommonDimensions::y))>>
constexpr void setY(U&& _value) noexcept
{
set(CommonDimensions::y, std::forward<U>(_value));
}
/*!
* \brief gets Z value
*
* \remark This function will be available only, if this Vector has 3 or more dimensions.
* \tparam Index This is a little trick to make the std::enable_if a dependent name. Do not pass any other type than the default one.
* \return Returns value of Z.
*/
template <typename Index = std::size_t, typename = std::enable_if_t<std::greater<Index>()(DIM, static_cast<Index>(CommonDimensions::z))>>
constexpr decltype(auto) getZ() const noexcept
{
return (*this)[CommonDimensions::z];
}
/*!
* \brief sets Z value
*
* \remark This function will be available only, if this Vector has 3 or more dimensions.
*
* \tparam U must be implicit convertible to T
* \tparam Index This is a little trick to make the std::enable_if a dependent name. Do not pass any other type than the default one.
* \param _value The value.
*/
template <typename U, typename Index = std::size_t, typename = std::enable_if_t<std::greater<Index>()(DIM, static_cast<Index>(CommonDimensions::z))>>
constexpr void setZ(U&& _value) noexcept
{
set(CommonDimensions::z, std::forward<U>(_value));
}
/*!
* \brief member wise addition
*
* \param _other The other.
*
* \return A reference of this object.
*/
constexpr Vector& operator +=(const Vector& _other) noexcept
{
for (std::size_t i = 0; i < DIM; ++i)
m_Values[i] += _other.m_Values[i];
return *this;
}
/*!
* \brief member wise subtraction
*
* \param _other The other.
*
* \return A reference of this object.
*/
constexpr Vector& operator -=(const Vector& _other) noexcept
{
for (std::size_t i = 0; i < DIM; ++i)
m_Values[i] -= _other.m_Values[i];
return *this;
}
/*!
* \brief member wise addition
*
* \param _val The value.
*
* \return A reference of this object.
*/
constexpr Vector& operator +=(const T& _val) noexcept
{
for (auto& el : m_Values)
el += _val;
return *this;
}
/*!
* \brief member wise subtraction
*
* \param _val The value.
*
* \return A reference of this object.
*/
constexpr Vector& operator -=(const T& _val) noexcept
{
for (auto& el : m_Values)
el -= _val;
return *this;
}
/*!
* \brief member wise multiplication
*
* \param _val The value.
*
* \return A reference of this object.
*/
constexpr Vector& operator *=(const T& _val) noexcept
{
for (auto& el : m_Values)
el *= _val;
return *this;
}
/*!
* \brief member wise division
*
* \param _val The value.
*
* \return A reference of this object.
*/
constexpr Vector& operator /=(const T& _val) noexcept
{
for (auto& el : m_Values)
el /= _val;
return *this;
}
/*!
* \brief member wise modulo
*
* \param _val The value.
*
* \return A reference of this object.
*/
constexpr Vector& operator %=(const T& _val) noexcept
{
for (auto& el : m_Values)
el %= _val;
return *this;
}
/*!
* \brief Equality operator
*
* \details performs a member wise equality check.
*
* \param _lhs The first instance to compare.
* \param _rhs The second instance to compare.
*
* \todo possibly better implementation when declared as constexpr: array equal check
* return _lhs.m_Values == _rhs.m_Values;
*
* \return True if the parameters are considered equivalent.
*/
friend constexpr bool operator ==(const Vector& _lhs, const Vector& _rhs) noexcept
{
for (std::size_t i = 0; i < DIM; ++i)
{
if (!(_lhs.m_Values[i] == _rhs.m_Values[i]))
return false;
}
return true;
}
private:
std::array<T, DIM> m_Values{};
};
operators arithmetic
namespace operators {
/*!
* \struct Plus
*
* \brief Single template helper struct for additions. Provides the inheritor with an implementation of operator +(const T&, const T&) as friend function.
* The inheritors must implement the operator +=(const T&, const T&) themselves.
*
* \tparam T Generic type parameter.
*/
template <class T>
struct Plus
{
/*!
* \brief Addition operator.
*
* \param _lhs The first value.
* \param _rhs A value to add to it.
*
* \return The result of the operation.
*/
friend constexpr T operator +(const T& _lhs, const T& _rhs)
{
T tmp(_lhs);
tmp += _rhs;
return tmp;
}
};
/*!
* \struct Minus
*
* \brief Single template helper struct for subtractions. Provides the inheritor with an implementation of operator -(const T&, const T&) as friend function.
* The inheritors must implement the operator -=(const T&, const T&) themselves.
*
* \tparam T Generic type parameter.
*/
template <class T>
struct Minus
{
/*!
* \brief Subtraction operator.
*
* \param _lhs The first value.
* \param _rhs A value to subtract from it.
*
* \return The result of the operation.
*/
friend constexpr T operator -(const T& _lhs, const T& _rhs)
{
T tmp(_lhs);
tmp -= _rhs;
return tmp;
}
};
/*!
* \struct Multiply
*
* \brief Single template helper struct for multiplications. Provides the inheritor with an implementation of operator *(const T&, const T&) as friend function.
* The inheritors must implement the operator *=(const T&, const T&) themselves.
*
* \tparam T Generic type parameter.
*/
template <class T>
struct Multiply
{
/*!
* \brief Multiplication operator.
*
* \param _lhs The first value to multiply.
* \param _rhs The second value to multiply.
*
* \return The result of the operation.
*/
friend constexpr T operator *(const T& _lhs, const T& _rhs)
{
T tmp(_lhs);
tmp *= _rhs;
return tmp;
}
};
/*!
* \struct Divide
*
* \brief Single template helper struct for divisions. Provides the inheritor with an implementation of operator /(const T&, const T&) as friend function.
* The inheritors must implement the operator /=(const T&, const T&) themselves.
*
* \tparam T Generic type parameter.
*/
template <class T>
struct Divide
{
/*!
* \brief Division operator.
*
* \param _lhs The numerator.
* \param _rhs The denominator.
*
* \return The result of the operation.
*/
friend constexpr T operator /(const T& _lhs, const T& _rhs)
{
T tmp(_lhs);
tmp /= _rhs;
return tmp;
}
};
/*!
* \struct Modulo
*
* \brief Single template helper struct for modulo. Provides the inheritor with an implementation of operator %(const T&, const T&) as friend function.
* The inheritors must implement the operator %=(const T&, const T&) themselves.
*
* \tparam T Generic type parameter.
*/
template <class T>
struct Modulo
{
/*!
* \brief Modulus operator.
*
* \param _lhs The numerator.
* \param _rhs The denominator.
*
* \return The result of the operation.
*/
friend constexpr T operator %(const T& _lhs, const T& _rhs)
{
T tmp(_lhs);
tmp %= _rhs;
return tmp;
}
};
/*!
* \struct Arithmetic
*
* \brief Single template helper struct for all arithmetic operations. Look at the inherited classes for more details.
*
* \tparam T Generic type parameter.
*/
template <class T>
struct Arithmetic :
Plus<T>,
Minus<T>,
Multiply<T>,
Divide<T>
{
};
/*!
* \struct Plus2
*
* \brief Multi template helper struct for additions. Provides the inheritor with an implementation of operator +(const T1&, const T2&) as friend function.
* The inheritors must implement the operator +=(const T1&, const T2&) themselves.
* \remark Because lack of consistency, the reversed operator +=(const T2&, const T1&) isn't implemented by this struct. If you need it, you either have to provide it by yourself
* or inherit Plus2<T2, T1>.
*
* \tparam T1 Generic type parameter.
* \tparam T2 Generic type parameter.
*/
template <class T1, class T2>
struct Plus2
{
/*!
* \brief Addition operator.
*
* \param _lhs The first value.
* \param _rhs A value to add to it.
*
* \return The result of the operation.
*/
friend constexpr T1 operator +(const T1& _lhs, const T2& _rhs)
{
T1 tmp(_lhs);
tmp += _rhs;
return tmp;
}
};
/*!
* \struct Minus2
*
* \brief Multi template helper struct for subtractions. Provides the inheritor with an implementation of operator -(const T1&, const T2&) as friend function.
* The inheritors must implement the operator -=(const T1&, const T2&) themselves.
* \remark Because lack of consistency, the reversed operator -=(const T2&, const T1&) isn't implemented by this struct. If you need it, you either have to provide it by yourself
* or inherit Minus2<T2, T1>.
*
* \tparam T1 Generic type parameter.
* \tparam T2 Generic type parameter.
*/
template <class T1, class T2>
struct Minus2
{
/*!
* \brief Subtraction operator.
*
* \param _lhs The first value.
* \param _rhs A value to subtract from it.
*
* \return The result of the operation.
*/
friend constexpr T1 operator -(const T1& _lhs, const T2& _rhs)
{
T1 tmp(_lhs);
tmp -= _rhs;
return tmp;
}
};
/*!
* \struct Multiply2
*
* \brief Multi template helper struct for multiplications. Provides the inheritor with an implementation of operator *(const T1&, const T2&) as friend function.
* The inheritors must implement the operator *=(const T1&, const T2&) themselves.
* \remark Because lack of consistency, the reversed operator *=(const T2&, const T1&) isn't implemented by this struct. If you need it, you either have to provide it by yourself
* or inherit Multiply2<T2, T1>.
*
* \tparam T1 Generic type parameter.
* \tparam T2 Generic type parameter.
*/
template <class T1, class T2>
struct Multiply2
{
/*!
* \brief Multiplication operator.
*
* \param _lhs The first value to multiply.
* \param _rhs The second value to multiply.
*
* \return The result of the operation.
*/
friend constexpr T1 operator *(const T1& _lhs, const T2& _rhs)
{
T1 tmp(_lhs);
tmp *= _rhs;
return tmp;
}
};
/*!
* \struct Divide2
*
* \brief Multi template helper struct for divisions. Provides the inheritor with an implementation of operator /(const T1&, const T2&) as friend function.
* The inheritors must implement the operator /=(const T1&, const T2&) themselves.
* \remark Because lack of consistency, the reversed operator /=(const T2&, const T1&) isn't implemented by this struct. If you need it, you either have to provide it by yourself
* or inherit Divide2<T2, T1>.
*
* \tparam T1 Generic type parameter.
* \tparam T2 Generic type parameter.
*/
template <class T1, class T2>
struct Divide2
{
/*!
* \brief Division operator.
*
* \param _lhs The numerator.
* \param _rhs The denominator.
*
* \return The result of the operation.
*/
friend constexpr T1 operator /(const T1& _lhs, const T2& _rhs)
{
T1 tmp(_lhs);
tmp /= _rhs;
return tmp;
}
};
/*!
* \struct Modulo2
*
* \brief Multi template helper struct for modulo. Provides the inheritor with an implementation of operator %(const T1&, const T2&) as friend function.
* The inheritors must implement the operator %=(const T1&, const T2&) themselves.
* \remark Because lack of consistency, the reversed operator %=(const T2&, const T1&) isn't implemented by this struct. If you need it, you either have to provide it by yourself
* or inherit Modulo2<T2, T1>.
*
* \tparam T1 Generic type parameter.
* \tparam T2 Generic type parameter.
*/
template <class T1, class T2>
struct Modulo2
{
/*!
* \brief Modulus operator.
*
* \param _lhs The numerator.
* \param _rhs The denominator.
*
* \return The result of the operation.
*/
friend constexpr T1 operator %(const T1& _lhs, const T2& _rhs)
{
T1 tmp(_lhs);
tmp %= _rhs;
return tmp;
}
};
/*!
* \struct Arithmetic2
*
* \brief Multi template helper struct for all arithmetic operations. Look at the inherited classes for more details.
*
* \tparam T1 Generic type parameter.
* \tparam T2 Generic type parameter.
*/
template <class T1, class T2>
struct Arithmetic2 :
Plus2<T1, T2>,
Minus2<T1, T2>,
Multiply2<T1, T2>,
Divide2<T1, T2>
{
};
} // namespace operators
operators equal
namespace operators {
/*!
* \struct Equal
*
* \brief Single template helper struct for compare equal. Provides the inheritor with an implementation of operator !=(const T&, const T&) as friend function.
* The inheritors must implement the operator ==(const T&, const T&) themselves.
*
* \tparam T Generic type parameter.
*/
template <class T>
struct Equal
{
/*!
* \brief Inequality operator
*
* \param _lhs The first instance to compare.
* \param _rhs The second instance to compare.
*
* \return True if the parameters are not considered equivalent.
*/
friend constexpr bool operator !=(const T& _lhs, const T& _rhs)
{
return !(_lhs == _rhs);
}
};
} // namespace operators
simple use-cases
constexpr Vector<int, 2> getVector()
{
Vector<int, 2> v(2); // initializes only x with 2
Vector<int, 2> p(1, 2);// inits x with 1 and y with 2
v.setX(2);
v.setY(2);
v += v;
v -= v;
v += 5;
v -= 3;
v *= 10;
v /= 2;
v %= 2;
auto t = v + 1; // copy construction
v = std::move(t); // move assign
auto x = v * 6; // move construction
auto d = v.getDimensions();
return v;
}
int main()
{
constexpr Vector<int, 2> vec(1, 2);
auto x = vec.getX();
auto y = vec.getY();
constexpr auto v(getVector()); // move construction
if constexpr (vec == v)
x = v.getX();
return 0;
}
1 Answer 1
What follows is an unstructured brain-dump; sorry for not giving this more detailed analysis.
I think that the X, Y and Z accessors add clutter to the class, and I would consider writing subclasses or wrappers of Vector<T, 2>
and Vector<T, 3>
to provide those. That's probably just a judgement call.
Can std::rel_ops
take the place of the Equals<>
mix-in class? They seem to do the same thing, but one is provided and well-known. If not, then also consider Boost.operators
(if Boost is acceptable to you).
Why inherit from Plus
and Minus
individually, but from Arithmetic2
en bloc? And is it intentional that Arithmetic2
doesn't inherit from Modulo2
, or is that an oversight?
Multiplication by a scalar should be commutative: if v * 6
is legal, why not 6 * v
?
The std::enable_if
on the template constructor looks too constrained - I think you should be able to std::enable_if_t<std::is_assignable<T,std::common_type_t<Args...>>
, but maybe I misunderstand what you're trying to do here. I'd have expected a simple
template<typename... Args>
constexpr explicit Vector(Args&&... args) noexcept
: m_Values{{std::forward<Args>(args)...}}
and rely on std::array
rejecting ill-formed construction.
Usually operator[]
returns a reference, at least for the non-const
overload (but that appears to be missing). Instead of a template, it's probably better to provide overloads for std::size_t
and CommonDimensions
arguments - or a conversion from the latter to the former.
Also, operator[]
conventionally doesn't check array bounds - bounds-checking interfaces are called at()
in the standard library, and it makes sense to follow the same conventions.
Consider making a Vector
act as a standard container - provide size()
as a synonym (or replacement) for getDimensions()
. It may well be worth exposing some public typedefs for value_type
and similar.
It's a shame that std::array::operator==
can't propagate constexpr
/noexcept
from its value_type::operator==
, or we'd be able to write
constexpr bool operator ==(const Vector& other) const noexcept
{
return m_Values == other.m_Values;
}
-
\$\begingroup\$ Thanks for your answer. My operator structs do almost the same as
boost::operators
(but mine areconstexpr
). I don't wanted to add this type of dependency to my privat lib, thus I decided to implement this myself. I didn't knowstd::rel_ops
, but they don't seem to be usable in an elegant way. The missingModulo(2)
for theArithmetic(2)
is a mistake, thanks for this! Yeah, an overload of CommonDimensions would be a better choice than the template! Multiplication and Addition are both commutative; shall I add those operators? Is it practical? Because-
,/
and%
aren't... \$\endgroup\$DNKpp– DNKpp2017年12月21日 21:05:05 +00:00Commented Dec 21, 2017 at 21:05 -
\$\begingroup\$ Most of your questions in the comment are choices - I've made observations, but I'm consciously shying away from recommendations. Use your judgement. If you think that unary
-
would make sense, then I'd suggest implementingT - Vector
operator. TBH, I don't see a use case for adding aT
to every element (and I like my utility classes to be informed by how I need to use them). N.B. I'm not saying it has no value; just that I'm ignorant of it. \$\endgroup\$Toby Speight– Toby Speight2017年12月22日 09:06:16 +00:00Commented Dec 22, 2017 at 9:06 -
\$\begingroup\$ I'm not sure why you find
std::rel_ops
inelegant - perhaps construct a new question around that? (Is it because of constexpr? Or is it difficulty with importing it into the right namespace for ADL to work properly?). \$\endgroup\$Toby Speight– Toby Speight2017年12月22日 09:08:07 +00:00Commented Dec 22, 2017 at 9:08 -
\$\begingroup\$ Be wary of suggesting
std::rel_ops
: reddit.com/r/cpp/comments/7c5h80/… \$\endgroup\$Justin– Justin2017年12月22日 23:25:03 +00:00Commented Dec 22, 2017 at 23:25 -
\$\begingroup\$ @TobySpeight, Justin is right. Lets wait for three way comparison (
<=>
), it should make it in C++20. \$\endgroup\$Incomputable– Incomputable2017年12月23日 16:47:37 +00:00Commented Dec 23, 2017 at 16:47
Explore related questions
See similar questions with these tags.
operators::Plus
and the like? Why does functionality need to be inherited? That could make sense if you are planning also a matrix class, but if you do you might want to make the vector a specialization of the matrix. \$\endgroup\$namespace operators
, various#include
lines, etc. If you have (or could create) a short test program that shows how one would use the class, then that would also be worth adding to the question. \$\endgroup\$