Posted
Comments 0

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

“Homogeneous coordinates are ubiquitous in computer graphics because they allow common vector operations such as translation, rotation, scaling and perspective projection to be represented as a matrix by which the vector is multiplied.” wikipedia.org/Homogeneous_coordinates

Thus we introduce the HPoint class as a 4-ordinate vector representation, i.e. x,y,z with a w divisor.

class XYZW_Const
{
public:
	const double x,y,z,w;

	XYZW_Const(double a,double b,double c,double d):x(a),y(b),z(c),w(d) { }

	void Set(const XYZW_Const& xyzw)
	{	const_cast<double&>(x)=xyzw.x;
		const_cast<double&>(y)=xyzw.y;
		const_cast<double&>(z)=xyzw.z;
		const_cast<double&>(w)=xyzw.w;
	}
	void SetW(double ww)
	{	const_cast<double&>(w)=ww;
	};
};

Very similar to the XYZ triples, this representational class is very similar to XYZ_Const except it has a fourth double, and a dedicated method to set the fourth element, w.

The introduction of a fourth element means the VectorBase class needs tweaking to cater for it (whilst remaining compatible with triple element representations). We can do that by adding a static w member in the XYZ and XYZ_Const classes. Thus XYZ becomes:

class XYZ	// For Vector & Point
{
public:
	double x,y,z;
	static const double w;	// Dummy w

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

const double XYZ::w=0.0;

So, for triples, the 4 element constructor ignores the 4th element, and the w member exists only as a static constant. It can still be referenced as if a member variable, but without increasing the size of the object. This is one of the few examples where it is indispensable to be able to use the dot operator to access a static member (instead of the :: class scoping operator).

The HPoint class is similar to the Point class, except that while they can still be scaled by a scalar, it makes less sense to permit them to be directly translated by vectors. Translation is best left to homegenous transforms.

There are some query functions appropriate to HPoint, i.e. to query whether it can be converted to a Point (w!=0), whether it is a direction (point at infinity), whether it is the origin, and whether it is valid (invalid being null – origin at infinity).

The implicit conversion of an HPoint to a Point simply involves dividing x,y,z,w by w, i.e. such that w'=1.

Here is the code for the HPoint class:

class HPoint: protected VectorBase<XYZW_Const>
{
public:
protected:
	explicit HPoint(const VectorBase<XYZW_Const>& xyzw):VectorBase<XYZW_Const>(xyzw) {  }
public:
	using VectorBase<XYZW_Const>::x;	// READ ONLY
	using VectorBase<XYZW_Const>::y;	// For write access, use a Point.
	using VectorBase<XYZW_Const>::z;	//	
	using VectorBase<XYZW_Const>::w;	//

	static const HPoint origin;	// [0,0,0,1]	NB Origin is xyz=0 for any non-zero value of w

	using VectorBase<XYZW_Const>::SetW;

	// Constructors
	HPoint(double x,double y,double z,double w=1):VectorBase<XYZW_Const>(x,y,z,w) { }
	HPoint(const Point& p):VectorBase<XYZW_Const>(p.x,p.y,p.z,1) { }

	// Copy & Assign
	HPoint(const HPoint& p):VectorBase<XYZW_Const>(p) { }
	HPoint& operator=(const HPoint& p) { assign(p); return *this; }

	operator Point() const
	{	if (IsPoint()) return Point(x/w,y/w,z/w);
		throw std::runtime_error("Cannot convert HPoint at infinity to Point");
	}

	bool IsPoint() const { return w; }	// Point not at infinity simply requires non=zero w
	bool IsDirection() const { return !w&&!is_null(); }	// Direction (point at infinity) requires w=0, with non-zero xyz
	bool IsOrigin() const { return w&&is_null(); }	// Origin point if non-zero w, and xyz=0
	bool IsValid() const { return w||!is_null(); }	// Valid if non-zero w, or w=0 && xyz!=0
	explicit operator bool() const { return w||!is_null(); }	// True if any element non-zero, i.e. valid
	bool operator!() const { return !w&&is_null(); }	// True if all elements zero, i.e. invalid

	// HPoints may be transformed by a scalar
	HPoint& operator*=(double m) { multiply_by(m); return *this; }

	HPoint& operator/=(double d) { SetW(w*d); return *this; }

	friend HPoint operator*(const HPoint& p,double m) { return HPoint(p.multiplication(m)); }
	friend HPoint operator*(double m,const HPoint& p) { return HPoint(p.multiplication(m)); }
	friend HPoint operator/(const HPoint& p,double d) { return HPoint(p)/=d; }

	friend std::ostream& operator<<(std::ostream& os,const HPoint& p) { return os<<'['<<p.x<<','<<p.y<<','<<p.z<<','<<p.w<<']'; }	// E.g. [1,2,3,1]
};

const HPoint HPoint::origin=HPoint(0,0,0);

And here is a little test program:

int main()	// The program
{
	const Point x(1,2,3);
	const HPoint p(7,3,5,2),q(x);

	cout<<"p="<<p<<"\n";
	cout<<"q="<<q<<"\n";

	HPoint r=p/4;

	cout<<"r=p/4 -> r="<<r<<"\n";

	if (r.IsPoint())
		cout<<"r is point\n";
	else
		cout<<"r is not point\n";

	r/=0;

	cout<<"r/=0  -> r="<<r<<"\n";

	if (r.IsDirection())
		cout<<"r is direction\n";
	else
		cout<<"r is not direction\n";

	r=p*0;

	cout<<"r=p*0  -> r="<<r<<"\n";

	if (r.IsOrigin())
		cout<<"r is origin\n";
	else
		cout<<"r is not origin\n";


	r/=0;

	cout<<"r/=0  -> r="<<r<<"\n";

	if (r.IsValid())
		cout<<"r is valid\n";
	else
		cout<<"r is invalid\n";

	Point a=static_cast<Point>(p)+Vector(1,2,3);

	cout<<"p+[1,2,3]="<<a<<"\n";

	return 0;
}

Which produces the following output:

p=[7,3,5,2]
q=[1,2,3,1]
r=p/4 -> r=[7,3,5,8]
r is point
r/=0  -> r=[7,3,5,0]
r is direction
r=p*0  -> r=[0,0,0,2]
r is origin
r/=0  -> r=[0,0,0,0]
r is invalid
p+[1,2,3]=[4.5,3.5,5.5]

Author

Posted
Comments 0

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

There is one other common type of vector and this is the unit vector. It is by definition of unit magnitude, and thus intended for the purposes of indicating direction only. It has distinct semantics from the general vector, and the vector represented point.

While a UnitVector class would still be represented by three doubles (x,y,z), unlike either the Vector or Point, it makes no sense to permit the elements to be individually assigned (given their magnitude must always be unity). Therefore, one creates a new representational class where the elements are read-only, thus:

class XYZ_Const
{
public:
	const double x,y,z;

	XYZ_Const(double a,double b,double c):x(a),y(b),z(c) { }
	void Set(const XYZ_Const& xyz)
	{	const_cast<double&>(x)=xyz.x;
		const_cast<double&>(y)=xyz.y;
		const_cast<double&>(z)=xyz.z;
	}
};

It still has an assignment method, which must necessarily cast away the constness of the members in order to assign a new unit vector.

The implementation of the UnitVector is largely the same as the Vector, except that most methods are const, except for assignment.

To assure unit magnitude, constructors normalise supplied coordinates, however, they are not unity/sanity-checked, e.g. UnitVector(0.33,0.66,066) is not the UnitVector(0.33,0.66,0.66) that may have been intended.

Because a priori, a UnitVector has unity magnitude, an exception is thrown should it be attempted to construct one from a null vector (or null coordinates).

Typically used to represent direction, it may be helpful to provide constructors or methods that enable the creation of UnitVectors from angles of rotation, but I’ll leave these until later. However, at least the three axes can be defined as static constants.

A UnitVector is implicitly a vector, thus the implicit conversion operator is provided.

Code for the UnitVector class and an example program (with output) is below:

class UnitVector: protected VectorBase<XYZ_Const>
{
protected:
	explicit UnitVector(const VectorBase<XYZ_Const>& xyz):VectorBase<XYZ_Const>(xyz) { } 	// NOT CHECKED FOR UNITY
public:
	// Elements & Accessors

	using VectorBase<XYZ_Const>::x;	// READ ONLY
	using VectorBase<XYZ_Const>::y;	// [	Write access has unclear semantics.
	using VectorBase<XYZ_Const>::z;	//		How can you change one element of a unit vector? ]

	static const UnitVector axis_x;	// |(1,0,0)|
	static const UnitVector axis_y;	// |(0,1,0)|
	static const UnitVector axis_z;	// |(0,0,1)|

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

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

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

	// Constructors

	UnitVector(double a,double b,double c):VectorBase<XYZ_Const>(a,b,c) { normalise(); }	// Null vector will THROW
	UnitVector(const double d[3]):VectorBase<XYZ_Const>(d[0],d[1],d[2]) { normalise(); }				// Null vector will THROW

	explicit UnitVector(const Vector& v):VectorBase<XYZ_Const>(v.x,v.y,v.z) { normalise(); }			// Null vector will THROW

	// Assignment
	UnitVector& operator=(const UnitVector& u) { assign(u); return *this; }

	// Informational
	bool operator==(const UnitVector& u) const { return is_equal_to(u); }
	bool operator!=(const UnitVector& u) const { return !is_equal_to(u); }

	// Conversions
	operator const Vector& () const { return *reinterpret_cast<const Vector*>(this); }	// Implicitly a const vector

	// Scalar operations

	UnitVector operator-() const { return UnitVector(negation()); }	// Negation retains unity

	// Scalar operations resulting in a vector
	friend Vector operator*(const UnitVector& u,double s) { return Vector(u)*s; }
	friend Vector operator*(double s,const UnitVector& u) { return s*Vector(u); }
	friend Vector operator/(const UnitVector& u,double s) { return Vector(u)/s; }	// Throws div0

	friend std::ostream& operator<<(std::ostream& os,const UnitVector& u) { return os<<"|("<<u.x<<','<<u.y<<','<<u.z<<")|"; }

};

const UnitVector UnitVector::axis_x=UnitVector(1,0,0);
const UnitVector UnitVector::axis_y=UnitVector(0,1,0);
const UnitVector UnitVector::axis_z=UnitVector(0,0,1);

int main()	// The program
{	const Vector u(7,3,5),v(1,2,3);

	cout<<"u="<<u<<"\n";
	cout<<"v="<<v<<"\n";

	cout<<"u x v = "<<u.cross(v)<<"\n";

	UnitVector uv(u.cross(v));

	cout<<"Norm(u x v)="<<uv<<"\n";

	UnitVector h(0.33,0.66,066);

	cout<<"h = "<<h<<"\n";

	return 0;
}

Program output:

u=(7,3,5)
v=(1,2,3)
u x v = (-4,-9,11)
Norm(u x v)=|(-0.270914,-0.609557,0.745014)|
h = |(0.00611054,0.0122211,0.999907)|

Author

Posted
Comments 0

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

A point is a position in 3D space, and can be represented by a vector from the origin.

A point has subtly different semantics from a vector. A vector can represent the translation from any point to any other, but a point represents a specific position.

Thus one can subtract one point from another to obtain the vector between them, but it makes no sense to add one position to another. Of course, you can add a vector to a point to obtain a new point, or you can add two vectors to obtain a single equivalent vector.

Therefore the addition and subtraction operations for a point are restricted accordingly. The following lists these:

p'=p+v  (or p+=v )
p'=p-v  (or p-=v )

v'=p'-p

Note that p'=v+p is not catered for, being less readable. However, negation, as a scaling by -1, has been provided, despite readability – thus one can have p'=-p+v or v'=p - -p

Although Point is represented by a Vector, it is not a vector per se, however, one may often wish to convert it directly to its representational Vector (rather than obtaining this by subtracting the origin from it), therefore an explicit conversion operator is provided:

explicit operator const Vector& () const { return *reinterpret_cast<const Vector*>(this); }

In other respects the Point class implementation is very similar to that of the Vector class.

There follows the code for the Point class, an example program, and its output:

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

	using VectorBase<XYZ>::x;
	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

	Point(double a,double b,double c):VectorBase<XYZ>(a,b,c) { }

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

	explicit Point(const Vector& v):VectorBase<XYZ>(v.x,v.y,v.z) { }	// The point obtained by adding v to the origin

	static const Point origin;	// [0,0,0] - pre-constructed null

	// Assignment
	Point& operator=(const Point& p) { assign(p); return *this; }

	// Conversions
	explicit operator const Vector& () const { return *reinterpret_cast<const Vector*>(this); }	// Can be cast to a const vector

	// Informational

	explicit operator bool() const { return !is_null(); }	// True if not origin
	bool operator!() const { return is_null(); }

	bool operator==(const Point& p) const { return is_equal_to(p); }
	bool operator!=(const Point& p) const { return !is_equal_to(p); }

	// Operations

	// Points may be transformed by a scalar
	Point operator-() const { return Point(negation()); }	// Equivalent to scaling by -1

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

	friend Point operator*(const Point& p,double s) { return Point(p.multiplication(s)); }
	friend Point operator*(double s,const Point& p) { return Point(p.multiplication(s)); }
	friend Point operator/(const Point& p,double s) { return Point(p.division(s)); }	// Throws div0

	// Transform point by vector addition
	Point& operator+=(const Vector& v) { add(Point(v)); return *this; }
	Point& operator-=(const Vector& v) { subtract(Point(v)); return *this; }

	friend Point operator+(const Point& p,const Vector& v) { return Point(static_cast<const Vector&>(p)+v); }
	friend Point operator-(const Point& p,const Vector& v) { return Point(static_cast<const Vector&>(p)-v); }

	// Calculate vector between points
	friend Vector operator-(const Point& p,const Point& q) { return static_cast<const Vector&>(p)-static_cast<const Vector&>(q); }

	friend std::ostream& operator<<(std::ostream& os,const Point& p) { return os<<'['<<p.x<<','<<p.y<<','<<p.z<<']'; }	// E.g. [1,2,3]

};

const Point Point::origin=Point(0,0,0);

int main()	// The program
{	Point p=Point::origin;
	const Vector v(1,2,3);

	cout<<"p="<<p<<"\n";
	cout<<"v="<<v<<"\n";

	p+=v;

	cout<<"p+=v: "<<p<<"\n";

	p*=1.5;

	cout<<"p*=1.5: "<<p<<"\n";

	p-=v;

	cout<<"p-=v: "<<p<<"\n";

	return 0;
}

Program output:

p=[0,0,0]
v=(1,2,3)
p+=v: [1,2,3]
p*=1.5: [1.5,3,4.5]
p-=v: [0.5,1,1.5]

Author

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

Posted
Comments 0

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

What remains to implement for the Vector class is rather ancillary, i.e. constructors and conversions, element access, and informational functions.

The Vector is represented by three doubles: x,y,z. However, it is also an array of three doubles, i.e. double [3]. Although the Microsoft C++ compiler permits anonymous structs which would allow the use of a union to enable the superimposition of double x,y,z with double[3], for portability, I will use casts and conversion operators to implement this. The array is simply the address of the first element, i.e. &x. Accessing elements can be done by overloading the array element operator, which permits bounds checking. The helper methods are thus:

double* array() { return &x; }

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

These are also implemented in const form, but note that the const element method returns a const reference rather than a value – being an accessor rather than a function returning a value.

These become public operators of the Vector class as follows:

explicit operator const double* () const { return array(); }
explicit operator double* () { return array(); }

const double& operator[](int i) const { return element(i); }
double& operator[](int i) { return element(i); }

Array conversion operators must be made explicit, otherwise invalid vector arithmetic expressions are liable to be converted into pointer arithemtic expressions. Otherwise, a Vector is just as much an array as it is x,y,z.

Another point on arrays to note is that C++ does not encode the size of the array when matching arguments, i.e. an array of 4 doubles will match an array of 3. Thus the following constructor (from an array of 3 doubles) will take any sized array or pointer-to-double.

explicit Vector(const double p[3]):x(p[0]),y(p[1]),z(p[2]) { }

Thus, it should be explicit to improve readability and safety, e.g. an array of three doubles (or object with an implicit conversion to such) is not necessarily a vector.

There is a special vector, which is the null vector. There are thus a few members concerned with this:

bool is_null() const { return !x&&!y&&!z; }
explicit operator bool() const { return !is_null(); }	// Don't want Vector implicitly converted to int
bool operator!() const { return is_null(); }
static const Vector null;

The ‘null’ member is initialised as const Vector Vector::null=Vector(0,0,0); and may sometimes be useful (referenced via Vector::null).

I have implemented an explicit cast of a vector to a double, by way of providing a means to obtaining the vector’s magnitude. I would argue that this is a valid, and unsurprising explicit conversion of a vector to a scalar.

For other methods such as equality comparison, I leave you to locate these in the code below:

#include <iostream>	// For console output functionality

using namespace std;	// Avoids having to use std:: scoping prefix

class Vector	// A vector class with addition and subtraction functionality
{
protected:
	// Accessor helpers
	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 v[3]) const { v[0]=x; v[1]=y; v[2]=z; return v; }	// Copies this vector to supplied array

	// Non-modifying helpers
	bool is_null() const { return !x&&!y&&!z; }
	bool is_equal_to(const Vector& 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()); }
	Vector negation() const { return Vector(-x,-y,-z); }
	Vector normalisation() const { return division(magnitude()); }
	Vector addition(const Vector& v) const { return Vector(x+v.x,y+v.y,z+v.z); }
	Vector subtraction(const Vector& v) const { return Vector(x-v.x,y-v.y,z-v.z); }
	Vector multiplication(double m) const { return Vector(x*m,y*m,z*m); }
	Vector division(double d) const { if (d) return Vector(x/d,y/d,z/d); else throw std::runtime_error("Divide by Zero"); }
	double dot_product(const Vector& v) const { return v.x*x+v.y*y+v.z*z; }
	Vector cross_product(const Vector& v) const { return Vector(v.y*y-v.y*z,v.x*z-v.y*x,v.y*x-v.x*y); }

	// Modifying helpers
	void assign(const Vector& v) { x=v.x; y=v.y; z=v.z; }
	void add(const Vector& v) { assign(addition(v)); }
	void subtract(const Vector& v) { assign(subtraction(v)); }
	void multiply_by(double m) { assign(multiplication(m)); }
	void divide_by(double d) { assign(division(d)); }

public:
	// Elements and accessors
	double x,y,z;	// Representation

	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 p[3]) const { return read(p); }

	// Constructors
	Vector(double a,double b,double c):x(a),y(b),z(c) { }
	explicit Vector(const double p[3]):x(p[0]),y(p[1]),z(p[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 m) { multiply_by(m); return *this; }
	Vector& operator/=(double d) { divide_by(d); return *this; }	// Throws div0

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

	// 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 cross_product(v); }

	// Output
	friend ostream& operator<<(ostream& os,const Vector& v)
	{ return os<<'('<<v.x<<','<<v.y<<','<<v.z<<')'; }	// Output, e.g. (1,2,3)
};

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

int main()	// The program
{	const Vector u(1,2,3);	// A test vector
	double d[4]={6,4,3,1};
	Vector v(d);	// A test vector

	cout<<"u="<<u<<"\n";
	cout<<"v="<<v<<"\n";

	double t0=v[0],t1=*(v+1);

	cout<<"t0,t1="<<t0<<","<<t1<<"\n";

	Vector v0=Vector::null;

	cout<<"v0="<<v0<<"\n";

	if (v0)
		cout<<"v0 is true\n";
	else
		if (!v0)
			cout<<"v0 is false\n";
		else
			cout<<"v0 is neither true nor false\n";

	return 0;
}

Program output is:

u=(1,2,3)
v=(6,4,3)
t0,t1=6,4
v0=(0,0,0)
v0 is false

Author