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?
2 Answers 2
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)
.
-
\$\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 insideVector
)? \$\endgroup\$AntiElephant– AntiElephant2015年10月19日 23:42:47 +00:00Commented 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 writetemplate<int N> void foo(Vector<N> v){ ... }
and the function will work on all vectors. \$\endgroup\$Winther– Winther2015年10月20日 00:04:45 +00:00Commented Oct 20, 2015 at 0:04
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& );