Vector - Separation of Concerns

Posted
Comments 0

Part 6 of C++ tutorial – a 3D vector & transform library

With the expectation that we will need other 3D data types that are vectors or very similar, such as a 3D point, it is time to separate the concerns of the Vector class into representation, helper methods, and implementation. That way we can just tweak the implementation depending upon behavioural and semantic differences.

The representation comprises three doubles, i.e. double x,y,z; and this can be written quite simply:

class XYZ
{
public:
	double x,y,z;

	XYZ(double a,double b,double c):x(a),y(b),z(c) { }
	void Set(const XYZ& xyz) { *this=xyz; }
};

It just needs a constructor and a representation dependent assignment method.

The class of helper methods can then derive from this representation, but to avoid multiple definitions (per base representation), it should be a template, with the representation as the base class parameter. I’ll call the class VectorBase. You can see that the class is essentially identical to the set of protected methods of the non-template Vector class in the previous article, except that Vector is changed to VectorBase and the public member variables of the representation base class must be made visible via the using keyword.

template <class XYZ_CLASS>
class VectorBase: public XYZ_CLASS
{
public:

	// Elements & accessors
	using XYZ_CLASS::x;
	using XYZ_CLASS::y;
	using XYZ_CLASS::z;

	const double* array() const { return &x; }
	double* array() { return &x; }

	const double& element(int i) const	// Const array element accessor
	{
		switch (i)
		{
		case 0: return x; case 1: return y; case 2: return z;
		default: throw std::out_of_range("Element index");
		}
	}

	double& element(int i) // Non-const array element accessor - for assignment
	{
		switch (i)
		{
		case 0: return x; case 1: return y;	case 2: return z;
		default: throw std::out_of_range("Element index");
		}
	}

	double* read(double d[3],int i=3) const
	{
		switch (i)
		{
		case 3: d[2]=z;	case 2: d[1]=y;	case 1: d[0]=x;
		case 0:	return d;
		default: throw std::out_of_range("Element count");
		}
	}

	// Constructors

	VectorBase(double a,double b,double c): XYZ_CLASS(a,b,c) { }

	// Const Functions - concerning xyz only
	bool is_null() const { return !x&&!y&&!z; }
	bool is_equal_to(const VectorBase& v) const { return x==v.x&&y==v.y&&z==v.z; }
	double magnitude_squared() const { return x*x+y*y+z*z; }
	double magnitude() const { return sqrt(magnitude_squared()); }
	VectorBase negation() const { return VectorBase(-x,-y,-z); }
	VectorBase normalisation() const { return division(magnitude()); }
	VectorBase multiplication(double s) const { return VectorBase(x*s,y*s,z*s); }
	VectorBase division(double s) const { if (s) return VectorBase(x/s,y/s,z/s); else throw std::runtime_error("Divide by Zero"); }
	VectorBase addition(const VectorBase& v) const { return VectorBase(x+v.x,y+v.y,z+v.z); }
	VectorBase subtraction(const VectorBase& v) const { return VectorBase(x-v.x,y-v.y,z-v.z); }
	double dot_product(const VectorBase& v) const { return x*v.x+y*v.y+z*v.z; }
	VectorBase cross_product(const VectorBase& v) const { return VectorBase(y*v.y-v.y*z,v.x*z-x*v.y,x*v.y-v.x*y); }

	// Modifying transforms
	using XYZ_CLASS::Set;

	void assign(const VectorBase& v) { Set(v); }
	void add(const VectorBase& v) { assign(addition(v)); }
	void subtract(const VectorBase& v) { assign(subtraction(v)); }
	void negate() { assign(negation()); }
	void multiply_by(double s) { assign(multiplication(s)); }
	void divide_by(double s) { assign(division(s)); }
	void normalise() { assign(normalisation()); }
};

The Vector class is now more concise, now that the representation and helper methods can be inherited from the template base. It differs from the previous Vector class in that it now needs to have a constructor from its base (VectorBase<XYZ>), must also make the XYZ representation visible via using, and must initialise its base within constructors.

Note that the VectorBase<XYZ> base class is protected, which means it is not publicly visible, there is not a public ‘is a’ relationship between Vector and VectorBase<XYZ>. It is only visible to Vector (and derivatives) for implementation purposes.

class Vector: protected VectorBase<XYZ>
{
protected:
	explicit Vector(const VectorBase<XYZ>& xyz):VectorBase<XYZ>(xyz) { }
public:
	// Elements & accessors

	using VectorBase<XYZ>::x;	// 'using' otherwise hidden
	using VectorBase<XYZ>::y;
	using VectorBase<XYZ>::z;

	explicit operator const double* () const { return array(); }	// Cast to const array
	explicit operator double* () { return array(); }	// Cast to non-const array

	const double& operator[](int i) const { return element(i); }	// Const array element accessor
	double& operator[](int i) { return element(i); } // Non-const array element accessor - for assignment

	double* Read(double d[3]) const { return read(d); }

	// Constructors

	// Vector() { }	// Explicit initialisation de rigeur, e.g. Vector v=Vector::null;
	Vector(double a,double b,double c):VectorBase<XYZ>(a,b,c) { }

	explicit Vector(const double d[3]):VectorBase<XYZ>(d[0],d[1],d[2]) { }

	static const Vector null;

	// Assignment
	Vector& operator=(const Vector& v) { assign(v); return *this; }

	// Informational
	explicit operator bool() const { return !is_null(); }	// Don't want Vector implicitly converted to int or bool
	bool operator!() const { return is_null(); }

	explicit operator double() const { return magnitude(); }

	bool operator==(const Vector& v) const { return is_equal_to(v); }
	bool operator!=(const Vector& v) const { return !is_equal_to(v); }

	// Scalar operations
	Vector operator-() const { return Vector(negation()); }

	Vector& operator*=(double s) { multiply_by(s); return *this; }
	Vector& operator/=(double s) { divide_by(s); return *this; }	// Throws div0

	friend Vector operator*(const Vector& v,double s) { return Vector(v.multiplication(s)); }
	friend Vector operator*(double s,const Vector& v) { return Vector(v.multiplication(s)); }
	friend Vector operator/(const Vector& v,double s) { return Vector(v.division(s)); }

	// Vector operations
	Vector& operator+=(const Vector& v) { add(v); return *this; }
	Vector& operator-=(const Vector& v) { subtract(v); return *this; }

	friend Vector operator+(const Vector& u,const Vector& v) { return Vector(u.addition(v)); }
	friend Vector operator-(const Vector& u,const Vector& v) { return Vector(u.subtraction(v)); }

	double dot(const Vector& v) const { return dot_product(v); }
	Vector cross(const Vector& v) const { return Vector(cross_product(v)); }	// NB Result is Normal vector


	friend std::ostream& operator<<(std::ostream& os,const Vector& v) { return os<<'('<<v.x<<','<<v.y<<','<<v.z<<')'; }

};

const Vector Vector::null=Vector(0,0,0);	// Standard null vector (0,0,0)

Author

Comments

There are currently no comments on this article.

Comment

Enter your comment below. Fields marked * are required. You must preview your comment before submitting it.