3
\$\begingroup\$

I wrote a template class for a 2D vector that is intended to provide 2D vector arithmetic to be used in a Physics simulation program. Note that x and y are public or ease of use since encapsulation here would just decrease the readability of code that uses the class.

Is the general style of my code good and does it comply with modern C++ best practices? Thank you for the suggestions.

template <typename T>
class Vector2D final {
public:
    T x;
    T y;

    // Defaulted constructors
    Vector2D(const Vector2D<T>& v) = default;
    ~Vector2D() = default;

    Vector2D(T ix, T iy);

    // Member Utility Functions
    void Set(T ix, T iy);
    T Magnitude();

    // Overloaded Compound Operators
    Vector2D<T>& operator+=(const Vector2D<T>& rhs);
    Vector2D<T>& operator*=(const Vector2D<T>& rhs);
    Vector2D<T>& operator-=(const Vector2D<T>& rhs);
    Vector2D<T>& operator/=(const T& rhs);
    Vector2D<T>& operator=(const  Vector2D<T>& rhs);
};

// Non-Compound Operators.
template <typename T> Vector2D<T> operator+(Vector2D<T> lhs, const Vector2D<T>& rhs);
template <typename T> Vector2D<T> operator*(Vector2D<T> lhs, const Vector2D<T>& rhs);
template <typename T> Vector2D<T> operator-(Vector2D<T> lhs, const Vector2D<T>& rhs);
template <typename T> Vector2D<T> operator/(Vector2D<T> lhs, const T& rhs);

template <typename T>
Vector2D<T>::Vector2D(T ix, T iy)
        : x(ix)
        , y(iy)
{
}

template <typename T>
void Vector2D<T>::Set(T ix, T iy) {
    x = ix;
    y = iy;
}

template <typename T>
T Vector2D<T>::Magnitude() {
    return x*x + y*y;
}

template <typename T>
Vector2D<T>& Vector2D<T>::operator+=(const Vector2D<T>& rhs) {
    x += rhs.x;
    y += rhs.y;
    return *this;
}

template <typename T>
Vector2D<T>& Vector2D<T>::operator*=(const Vector2D<T>& rhs) {
    x *= rhs.x;
    y *= rhs.y;
    return *this;
}

template <typename T>
Vector2D<T>& Vector2D<T>::operator-=(const Vector2D<T>& rhs) {
    x -= rhs.x;
    y -= rhs.y;
    return *this;
}

template <typename T>
Vector2D<T>& Vector2D<T>::operator/=(const T& rhs) {
    x /= rhs;
    y /= rhs;
    return *this;
}

template <typename T>
Vector2D<T>& Vector2D<T>::operator=(const Vector2D<T>& rhs) {
    x = rhs.x;
    y = rhs.y;
    return *this;
}

template <typename T>
Vector2D<T> operator+(Vector2D<T> lhs, const Vector2D<T>& rhs) {
    lhs += rhs;
    return lhs;
}

template <typename T>
Vector2D<T> operator*(Vector2D<T> lhs, const Vector2D<T>& rhs) {
    lhs *= rhs;
    return lhs;
}

template <typename T>
Vector2D<T> operator-(Vector2D<T> lhs, const Vector2D<T>& rhs) {
    lhs -= rhs;
    return lhs;
}

template <typename T>
Vector2D<T> operator/(Vector2D<T> lhs, const T& rhs) {
    lhs /= rhs;
    return lhs;
}

Some examples of using this with float. I intend to only use it with floating point types (float, double, long double etc) and have tested it with other types but will present float here for brevity.

Vector2D<float> v1 = {1.0, 2.0};
Vector2D<float> v2 = {2.0, 3.0};

v1 += v2;
assert(v1.x == 3.0 && v1.y == 5.0);
v1 -= v2;
assert(v1.x == 1.0 && v1.y == 2.0);
v1 *= v2;
assert(v1.x == 2.0 && v1.y == 6.0);
v1 /= 2.0;
assert(v1.x == 1.0 && v1.y == 3.0);

v1.Set(1.0, 2.0);
v2.Set(2.0, 3.0);
Vector2D<float> v3 = {0.0, 0.0};

v3 = v1 + v2;
assert(v3.x == 3.0 && v3.y == 5.0);
v3 = v1 - v2;
assert(v3.x == -1.0 && v3.y == -1.0);
v3 = v1 * v2;
assert(v3.x == 2.0 && v3.y == 6.0);
v3 = v1 / 2.0f;
assert(v3.x == 0.5 && v3.y == 1.0);
\$\endgroup\$
3
  • \$\begingroup\$ What are your testcases? \$\endgroup\$
    – Mast
    Commented Jul 3, 2018 at 18:39
  • \$\begingroup\$ I updated to show use cases. I am not too concerned about the functionality (though feel free to suggest any changes that should be made) but am more interested in whether I am following the correct methods for modern C++. I come from a C/Fortran background so C++ structure and OO design is very new to me. \$\endgroup\$
    – fhorrobin
    Commented Jul 3, 2018 at 19:17
  • \$\begingroup\$ Take a look at this answer: codereview.stackexchange.com/a/27112/97584 \$\endgroup\$ Commented Jul 3, 2018 at 19:27

2 Answers 2

1
\$\begingroup\$

This looks pretty good! I don't think you need to change much, but there are some things missing that you will likely need:

  1. normalize() or normal() method to either normalize a vector or return a normalized version of the vector. If you're doing physics, you'll need normalized vectors for lots of operations, probably.
  2. Scalar multiply (operator*(const T)). You'll likely need to scale vectors frequently and will do that by multiplying by a scalar.
  3. operator==() and/or operator!=(). You should define one in terms of the other.
  4. Possibly a squared magnitude. Sometimes taking the square root in the Magnitude() function is a bottleneck and you don't need it (for example to see if a point is within a given radius of another point). It can be faster to get the squared magnitude of the vector between the two points if that's something you do a lot.
\$\endgroup\$
1
  • \$\begingroup\$ Thank you, I agree with your suggestions and have implemented them. \$\endgroup\$
    – fhorrobin
    Commented Jul 3, 2018 at 21:35
1
\$\begingroup\$

I see a few things that might be improved with this code.

Provide a constexpr constructor

Right now, this line fails to compile:

constexpr Vector2D<float> unit{1,1};

This is easily solved by simply adding the keyword constexpr to the constructor.

Consider using std::enable_if

These lines currently compile just fine:

Vector2D<std::string> vstring{"nine", "seven"};
Vector2D<std::complex<float>> vc{ {3,4}, {5,12} };

Should they? If your intent was to only support arithmetic types, then you might want to make that explicit by using std::enable_if and std::is_arithmetic.

Consider creating a default constructor

Some things are made easier if there is a default constructor. For example, neither of these will compile right now:

std::vector<Vector2D<float>> v;  
std::vector<Vector2D<float>> v(10);

We can still create a vector of Vector2D but we have to supply our own initalizer:

std::vector<Vector2D<float>> v(10, {3,4});

Do you really need Set?

In the search for a minimally sufficient interface, I'd suggest that Set is probably not really needed since the data members are public.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the advice. I will take a look at these suggestions and how they will work with my code. \$\endgroup\$
    – fhorrobin
    Commented Jul 3, 2018 at 22:25

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.