Skip to content

05 Vector (Part 2)

Karn Kaul edited this page Oct 1, 2022 · 1 revision

Introduction

In Vector (Part 1) we created a barebones generic Vec to get us going. This chapter will evolve it into a formal class exposing only desired operations and operator overloads.

Code

Construction And Invariants

Once Vec makes its data member array private, it will need to provide interfaces for users to populate and access that data, including constructors. Since we want to keep this nice syntax that's already in use: uvec2{400U, 300U}, that's the first one to tackle. Formulating it is a bit tricky, since the number of arguments varies per Dim; forunately, variadics combined with constraints can help us get there in one shot:

template <std::same_as<Type>... T>
  requires(sizeof...(T) == Dim)
constexpr Vec(T const... t) : values{t...} {}

Add at and operator[](std::size_t) overloads:

constexpr Type& at(std::size_t index) {
  assert(index < Dim);
  return values[index];
}

constexpr Type const& at(std::size_t index) const {
  assert(index < Dim);
  return values[index];
}

constexpr Type& operator[](std::size_t index) { return at(index); }
constexpr Type const& operator[](std::size_t index) const { return at(index); }

And enable iteration through std::array's existing machinery:

using Storage = std::array<Type, Dim>;
using iterator = typename Storage::iterator;
using const_iterator = typename Storage::const_iterator;

constexpr iterator begin() { return values.begin(); }
constexpr iterator end() { return values.end(); }
constexpr const_iterator begin() const { return values.begin(); }
constexpr const_iterator end() const { return values.end(); }

Finally, inherit the new Vec constructor(s) in Rgb:

struct Rgb : Vec<std::uint8_t, 3> {
  using Vec::Vec;
  // ...
};

Previous code that used values directly will need to be updated (eg io.cpp), after which the main refactor should be complete.

Back in Vec, another constructor that's useful to have is one that takes a single Type and initializes all components to that value:

explicit constexpr Vec(Type const t) {
  for (Type& value : values) { value = t; }
}

Comparison

This can simply be defaulted in C++20:

bool operator(Vec const&) const = default;

Dot Product

Also called Inner Product, A . B yields a scalar sum of the product of each corresponding component of A and B: A.x * B.x + A.y * B.y + .... It also happens to be ||A||.||B||.cos(AB), and has a plethora of geometric interpretations and uses, including in graphics and gamedev, especially 3D. Incorporating that is quite straightforward:

template <typename Type, std::size_t Dim>
constexpr Type dot(Vec<Type, Dim> const& a, Vec<Type, Dim> const& b) {
  auto ret = Type{};
  Vec<Type, Dim>::for_each(a, [&b, &ret](std::size_t i, Type const& value) { ret += value * b[i]; });
  return ret;
}

Magnitude

Also called Length, a vector's magnitude is the square root of the sum of the squares of its components (Pythagoras). The square of this quantity is also useful (eg to compare against 0.0f), which is basically the vector dotted with itself. We add both functions:

template <typename Type, std::size_t Dim>
constexpr Vec<Type, Dim> sqr_mag(Vec<Type, Dim> const& vec) {
  return dot(vec, vec);
}

template <typename Type, std::size_t Dim>
Vec<Type, Dim> magnitude(Vec<Type, Dim> const& vec) {
  return std::sqrt(dot(vec, vec));
}

std::sqrt is not constexpr in C++20, so we lose it for that function (and anything that unconditionally calls it).

Operator Overloads

Being a mathematical type, Vec can support quite a few operations, including the standard +, -, *, / and their self-assignment variants. In summary, we need to define these:

op Vec:
  member: -
  friend: -
Vec op Vec:
  member: += -= *= /=
  friend: + - * /
Vec op Type;
  member: += -= *= /=
  friend: + - * /
Type op Vec:
  friend: + *

That's a lot of functions, so only one of each category is shown below:

constexpr Vec& operator-() requires(!std::is_unsigned_v<Type>) {
  for (auto& value : m_values) { value = -value; }
  return *this;
}

constexpr Vec& operator+=(Vec const& rhs) {
  for_each(*this, [&rhs](std::size_t i, Type& value) { value += rhs.at(i); });
  return *this;
}
// ...

constexpr Vec& operator-=(Type const type) {
  for_each(*this, [type](std::size_t, Type& value) { value -= type; });
  return *this;
}
// ...

Let's take a look at what the definitions of Vec op(Vec, Type) and Vec op(Vec, Vec) are going to be:

friend constexpr Vec operator+(Vec const& a, Vec const& b) {
  auto ret = a;
  ret += b;
  return ret;
}

friend constexpr Vec operator+(Vec const& a, Type const b) {
  auto ret = a;
  ret += b;
  return ret;
}

Hmm, they look lexically identical; if only we could use a template argument for the second parameter! :) We can and in fact will use a constrained template parameter to define both variants simultaneously. Let's use a concept instead of a noisy requires clause on each overload:

template <typename Type, std::size_t Dim>
class Vec; // forward declaration

template <typename T, typename Type, std::size_t Dim>
concept VecOrType = std::same_as<T, Type> || std::convertible_to<T, Vec<Type, Dim>>;

Using it for operator+ yields:

template <VecOrType<Type, Dim> T>
friend constexpr Vec operator+(Vec const& a, T const& b) {
  auto ret = a;
  ret += b;
  return ret;
}

Finally, the op(Type, Vec) variants. Since - and / don't make sense here, and + and * are both symmetric operators, we can simply flip the operands around them:

friend constexpr Vec operator*(Type const& a, Vec const b) {
  auto ret = b;
  ret *= a;
  return ret;
}

Normalize

Normalizing a vector essentially means dividing by its magnitude, to yield a unit vector - one whose length is exactly 1.0. Implementing this is trivial now that we have all the operator overloads defined, but since it involves division, we add an infinity check:

template <std::size_t Dim>
Vec<float, Dim> normalize(Vec<float, Dim> const& in, float const epsilon = 0.001f) {
  auto const mag = magnitude(in);
  if (std::abs(mag) < epsilon) { return {}; }
  return in / mag;
}

And Vec is now all ready!

Clone this wiki locally