3
\$\begingroup\$

Right now I have no knowledge of templates, but I just finished learning about inheritance, and wanted to apply it to a Vector3 class that I had already created. My thoughts were that vectors of different dimensions share some common operations, and so it should be possible to apply an inheritance hierarchy to this.

Ultimately I might want a program that can operate with vectors without knowing their dimension. As a result, all vectors inherit from an abstract base class Vector which contains the most general operations of vectors for all dimensions, such as normalise(), get_length() a bool conversion and << >> operators.

I've included just the header because I'm really concerned only with making sure my inheritance structure is adequate. The function definitions would take up a lot of room, and I don't think they are too important (although let me know if they are necessary).

Vector.h

#ifndef MATHSVectors
#define MATHSVectors

#include <iostream>
#include <utility> //pair

class Vector{ //abstract base class but we do expect users, so virtual functions are public
    friend std::ostream& operator<<(std::ostream&, const Vector&);
    friend std::istream& operator>>(std::istream&, Vector&);
protected: //needed by derived classes
    Vector() { }
    Vector(const Vector&) = default;
    Vector(Vector&&) = default;
    Vector& operator=(const Vector&) = default;
    Vector& operator=(Vector&&) = default;
public:
    static const double pi;
    virtual double get_length() const = 0;
    virtual Vector& normalise() = 0;
    virtual explicit operator bool() const = 0;
    virtual ~Vector() { } //needed if we dynamically allocate
private: //used for operator<< and operator>>
    virtual std::ostream& print(std::ostream&) const = 0;
    virtual std::istream& read(std::istream&) = 0;
};

std::ostream& operator<<(std::ostream&, const Vector&);
std::istream& operator>>(std::istream&, Vector&);







class Vector2 : public Vector {
    friend bool operator==(const Vector2&, const Vector2&);
    friend bool operator!=(const Vector2&, const Vector2&);
    friend Vector2 operator+(const Vector2&, const Vector2&);
    friend Vector2 operator-(const Vector2&, const Vector2&);
    friend Vector2 operator*(const Vector2&, double);
    friend Vector2 operator*(double, const Vector2&);
    friend Vector2 operator/(const Vector2&, double);
    friend double dot_product(const Vector2&, const Vector2&);
public:

    Vector2() = default;
    Vector2(double a, double b): x(a), y(b) { }

    explicit operator bool() const override;

    Vector2& operator+=(const Vector2&);
    Vector2& operator-=(const Vector2&);
    Vector2& operator*=(double);
    Vector2& operator/=(double);

    double get_length() const override;
    Vector2& normalise() override;
    Vector2& rotateXY(double); //radians

    Vector2& setX(double a) { x = a; return *this;}
    Vector2& setY(double b) { y = b; return *this;}

    double getX() const { return x;}
    double getY() const { return y;}

private:
    std::ostream& print(std::ostream&) const override;
    std::istream& read(std::istream&) override;
    std::pair<double, double> rotate(double, double, double);
    double x = 0, y = 0;

};

bool operator==(const Vector2&, const Vector2&);
bool operator!=(const Vector2&, const Vector2&);
Vector2 operator+(const Vector2&, const Vector2&);
Vector2 operator-(const Vector2&, const Vector2&);
Vector2 operator*(const Vector2&, double);
Vector2 operator*(double, const Vector2&);
Vector2 operator/(const Vector2&, double);
double dot_product(const Vector2&, const Vector2&);








class Vector3 : public Vector {
    friend bool operator==(const Vector3&, const Vector3&);
    friend bool operator!=(const Vector3&, const Vector3&);
    friend Vector3 operator+(const Vector3&, const Vector3&);
    friend Vector3 operator-(const Vector3&, const Vector3&);
    friend Vector3 operator*(const Vector3&, double);
    friend Vector3 operator*(double, const Vector3&);
    friend Vector3 operator/(const Vector3&, double);
    friend double dot_product(const Vector3&, const Vector3&);
    friend Vector3 cross_product(const Vector3&, const Vector3&);
public:

    Vector3() = default;
    Vector3(double a, double b, double c): x(a), y(b), z(c) { }

    explicit operator bool() const override;

    Vector3& operator+=(const Vector3&);
    Vector3& operator-=(const Vector3&);
    Vector3& operator*=(double);
    Vector3& operator/=(double);

    double get_length() const override;
    Vector3& normalise() override;
    Vector3& rotateXY(double); //radians
    Vector3& rotateXZ(double);
    Vector3& rotateYZ(double);

    Vector3& setX(double a) { x = a; return *this;}
    Vector3& setY(double b) { y = b; return *this;}
    Vector3& setZ(double c) { z = c; return *this;}

    double getX() const { return x;}
    double getY() const { return y;}
    double getZ() const { return z;}

private:
    std::ostream& print(std::ostream&) const override;
    std::istream& read(std::istream&) override;
    std::pair<double, double> rotate(double, double, double);
    double x = 0, y = 0, z = 0;

};

bool operator==(const Vector3&, const Vector3&);
bool operator!=(const Vector3&, const Vector3&);
Vector3 operator+(const Vector3&, const Vector3&);
Vector3 operator-(const Vector3&, const Vector3&);
Vector3 operator*(const Vector3&, double);
Vector3 operator*(double, const Vector3&);
Vector3 operator/(const Vector3&, double);
double dot_product(const Vector3&, const Vector3&);
Vector3 cross_product(const Vector3&, const Vector3&);


#endif // Vector3

The one thing I'm concerned with is there might be a lot of redundancy in function definitions - e.g. of defining getX() or getY() separately in every class of every dimension. An idea might be to define Vector and then inherit Vector2 from that, and inherit Vector3 from Vector2, and so on, so that I have Vector(N+1) inheriting from Vector(N).

What I would be concerned with if using this method is that it reflects an odd relationship. Why would a Vector3 be used in place of a Vector2? - It would also mean I could compare, or add, a Vector3 to a Vector2 (through run-time binding) and expect a return value of a Vector2. I am not sure, to be consistent mathematically, whether I would want that.

One last note is that, since I want to refactor the most general operations of a vector in to the Vector class, I thought it would make sense to refactor arithmetic operators in to the Vector class. That left me with the issue (again) that I could add Vectors of different dimensions through derived-to-base conversions. The plain arithmetic operators would also return Vector objects, rather than references, so there could be no consistent virtuality.

I'm sure that once I learn about templates there might be an easier way to generalise a Vector function to n dimensions, but for now, is this looking okay?

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

This is a problem made for templates and using inheritance here is a bad idea. Instead of trying to make this work with inheritance I would stongly consider reading about templates. Templates are really simple to implement and for this code all you really need is one line of code template <unsigned int N> before class Vector to get ridd of the Vector2,Vector3,... classes:

template<unsigned int N>
class Vector{
   private:
     double x[N]; // Components of a general N-dim vector
   ...

   // Example of how to write a general function. You can use N
   // as a normal integer within the class
   void normalize(){
      double norm = 0.0;
      for(int i = 0; i < N; i++){
        norm += x[i]*x[i];
      }
      norm = 1.0/sqrt(norm);
      for(int i = 0; i < N; i++){
        x[i] *= norm;
      }
   }

   ...
}

You would then create a 2D vector by simply writing Vector<2> v;. This also gets ridd of all the code duplication you now have! As a bonus it will work for all N and you can also template over the type with little change to the code and get a vector that works with other types like float for free.


Some comments on your code:

As of now you have nothing useful in the base-class except for virtual function definitions and static const double pi so this class is not really neeed at all - it just adds unnessesary code. Also pi is not really part of a vector so it does not belong in the class. If you really need pi as a constant get it from the math library.

Instead of having the component separate as double x, y, z, ... I would use an array here double x[N]. If you do this then you can then replace the functions getX,getY,getZ with a single function

double getCoordinate(unsigned int i){
    if( i < N ) { 
       return x[i];
    } else {
       // Raise error, this is not a valid coordinate
       // ...
    }
}

Likewise you can replace setX,setY,setZ with setCoordinate(unsigned int i).

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the reply. Oh yeah I'm about to read up on templates, it's the next chapter of my book. I just wanted to apply my inheritance knowledge practically before I moved on. As for the redundancy of the base-class, what if I had some program that wanted to deal with a general vector without need to worry about its dimension. Would this not warrant the use of a base class so I could refer to any n-dimension vector object in this general sense (even if there was barely any code inside Vector)? \$\endgroup\$ Commented Oct 19, 2015 at 23:42
  • \$\begingroup\$ @AntiElephant Yes, I agree it's not completely redundant, but I don't think that its enough to warrant using inheritance here unless you want to do something crazy like adding a 2D and 3D vector:) But that is not how vectors are normally used so I would not worry about it in a general vector class. Note that one can also make a function that takes a general vector by using templates. E.g. if foo is some function that takes in a general vector then you can simply write template<int N> void foo(Vector<N> v){ ... } and the function will work on all vectors. \$\endgroup\$
    – Winther
    Commented Oct 20, 2015 at 0:04
1
\$\begingroup\$

Winther covers most of the important features. I just want to throw out a minor one. You currently have set* and get* functions. But much more typically used in C++ containers are operator[] and at(). The former doing zero range checking and the latter potentially throwing. I would suggest you do the same:

// no range checking
double& operator[](size_t idx) { return _data[idx]; }
const double& operator[](size_t idx) const { return _data[idx]; }

double& at(size_t idx) {
    if (idx >= N) throw std::out_of_range(...);
    return (*this)[idx];
}

const double& at(size_t idx) const {
    if (idx >= N) throw std::out_of_range(...);
    return (*this)[idx];
}

It's just more natural to write vec[0] = 4 than vec.setX(4) or vec.setCoordinate(0, 4).

Similarly, get_length() should be called either length() or size().


For normalization, I would also seriously consider this signature:

Vector normalize() const;

It will make code easier to reason about. Furthermore, this doesn't even need to be a member function:

template <size_t N>
Vector<N> normalized(Vector<N> const& );
\$\endgroup\$

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.