I've recently been fiddling around with a type safe implementation of cartesian co-ordinates (and a few operations on those co-ordinates). Often it's easy to get units mixed up: is something in metres, or in some other unit?
The idea for this code is to provide a framework to catch all such errors at compile time. This starts with a simple definition of the unit types we want to support:
distance.hpp
#ifndef UNITS_DISTANCE_HPP_
#define UNITS_DISTANCE_HPP_
#include <ostream>
enum struct distance
{
metre, kilometre, mile
};
std::ostream& operator<<(std::ostream& os, distance d)
{
switch (d) {
case distance::metre:
os << " metres";
break;
case distance::kilometre:
os << " kilometres";
break;
case distance::mile:
os << " miles";
break;
}
return os;
}
#endif
We then build up a quantity type that is templated on our distance type. This supports (explicit) conversions to and from quantity types, and a few simple operations:
quantity.hpp
#ifndef UNITS_QUANTITY_HPP
#define UNITS_QUANTITY_HPP
#include "distance.hpp"
#include <cmath>
#include <ostream>
namespace detail
{
// Defines a binary operation on quantities, mainly used
// to implement operator+ and operator-. Any binary operator
// implementation should forward to this class with the correct
// function (see the definition below).
template <distance D>
class binary_operation;
}
// Converts a distance in any format to metres, which is the
// "cannonical" distance unit. Everything is convertible to
// and from metres.
template <distance From>
struct to_metres;
// Performs a conversion between any distances, e.g.
// m -> km, m -> mi, km -> mi, etc.
template <distance To>
struct convert;
template <distance D>
class quantity;
// Given 3 distance points (of the same type), will calculate
// the euclidian distance. These points are expected
// to be calculated by (for example) (x2 - x1), (y2 - y1), (z2 - z1).
template <distance D>
quantity<D> euclid_distance(quantity<D> x, quantity<D> y, quantity<D> z);
template <distance D>
std::ostream& operator<<(std::ostream& os, const quantity<D>& q)
{
os << q.d_ << D;
return os;
}
template <distance D>
quantity<D> operator-(quantity<D> a);
// Class representing a distance quantity. Note that it is immutable.
template <distance D>
class quantity
{
private:
const double d_;
public:
explicit quantity(double d)
: d_(d)
{ }
quantity<D> scale(double by) const
{
return quantity<D>(d_ * by);
}
friend class detail::binary_operation<D>;
template <distance From>
friend struct to_metres;
template <distance To>
friend struct convert;
friend std::ostream& operator<< <>(std::ostream&, const quantity<D>&);
friend quantity<D> euclid_distance <>(quantity<D> x, quantity<D> y, quantity<D> z);
friend quantity<D> operator- <>(quantity<D> a);
};
using quantity_m = quantity<distance::metre>;
using quantity_km = quantity<distance::kilometre>;
using quantity_mi = quantity<distance::mile>;
quantity_m operator"" _m(long double d)
{
return quantity_m(d);
}
quantity_km operator"" _km(long double d)
{
return quantity_km(d);
}
quantity_mi operator"" _mi(long double d)
{
return quantity_mi(d);
}
template <distance D>
quantity<D> euclid_distance(quantity<D> x, quantity<D> y, quantity<D> z)
{
return quantity<D>(std::sqrt(x.d_ * x.d_ + y.d_ * y.d_ + z.d_ * z.d_));
}
template <>
struct to_metres<distance::metre>
{
quantity_m operator()(quantity_m value)
{
return value;
}
};
template <>
struct to_metres<distance::kilometre>
{
quantity_m operator()(quantity_km value)
{
return quantity_m(value.d_ * 1000.0);
}
};
template <>
struct to_metres<distance::mile>
{
static constexpr auto mile_to_metre = 1609.344;
quantity_m operator()(quantity_mi value)
{
return quantity_m(value.d_ * mile_to_metre);
}
};
template <>
struct convert<distance::metre>
{
template <distance From>
quantity_m operator()(quantity<From> value) const
{
return to_metres<From>()(value);
}
};
template <>
struct convert<distance::kilometre>
{
template <distance From>
quantity_km operator()(quantity<From> value) const
{
quantity_m metre_distance = to_metres<From>()(value);
return quantity_km(metre_distance.d_ / 1000.0);
}
};
template <>
struct convert<distance::mile>
{
static constexpr auto metre_to_mile = 0.000621371192;
template <distance From>
quantity_mi operator()(quantity<From> value) const
{
quantity_m metre_distance = to_metres<From>()(value);
return quantity_mi(metre_distance.d_ * metre_to_mile);
}
};
namespace detail
{
template <distance D>
class binary_operation
{
public:
template <typename Func>
static quantity<D> op(quantity<D> a, quantity<D> b, Func f)
{
return quantity<D>(f(a.d_, b.d_));
}
template <distance D2, typename Func>
static quantity<D> op(quantity<D> a, quantity<D2> b, Func f)
{
auto conv = convert<D>()(b);
return quantity<D>(f(a.d_, conv.d_));
}
};
}
template <distance D>
quantity<D> operator+(quantity<D> a, quantity<D> b)
{
return detail::binary_operation<D>::op(a, b, std::plus<double>());
}
template <distance D1, distance D2>
quantity<D1> operator+(quantity<D1> a, quantity<D2> b)
{
return detail::binary_operation<D1>::op(a, b, std::plus<double>());
}
template <distance D>
quantity<D> operator-(quantity<D> a, quantity<D> b)
{
return detail::binary_operation<D>::op(a, b, std::minus<double>());
}
template <distance D1, distance D2>
quantity<D1> operator-(quantity<D1> a, quantity<D2> b)
{
return detail::binary_operation<D1>::op(a, b, std::minus<double>());
}
template <distance D>
quantity<D> operator-(quantity<D> a)
{
return quantity<D>(-a.d_);
}
#endif
Similar code for angles:
angle.hpp
#ifndef UNITS_ANGLE_HPP_
#define UNITS_ANGLE_HPP_
#include <ostream>
enum struct angle_type
{
degree, radian
};
std::ostream& operator<<(std::ostream& os, angle_type a)
{
switch(a) {
case angle_type::degree:
return os << " degrees";
case angle_type::radian:
return os << " radians";
}
return os;
}
#endif
angle_impl.hpp
#ifndef UNITS_ANGLE_IMPL_HPP_
#define UNITS_ANGLE_IMPL_HPP_
#include "angle.hpp"
#include <cmath>
#include <ostream>
static constexpr auto pi = 3.1415926535;
template <angle_type A>
class angle;
template <angle_type A>
struct to_degree;
template <angle_type To>
struct convert_a;
template <angle_type A>
std::ostream& operator<<(std::ostream& os, angle<A> a)
{
os << a.value_ << A;
return os;
}
template <angle_type A>
class angle
{
private:
const double value_;
// This is a bit lazy, and would be ugly if we had more angle
// types.
template <typename Func>
double trig_func(Func f) const
{
if(A == angle_type::radian) {
return f(value_);
}
else if(A == angle_type::degree) {
auto in_rad = value_ * 180.0 / pi;
return f(in_rad);
}
}
public:
friend std::ostream& operator<< <>(std::ostream&, angle<A>);
friend class to_degree<A>;
template <angle_type To>
friend class convert_a;
explicit angle(double v)
: value_(v)
{ }
double sin() const
{
static auto sin_ = [](double d) { return std::sin(d); };
return trig_func(sin_);
}
double cos() const
{
static auto cos_ = [](double d) { return std::cos(d); };
return trig_func(cos_);
}
double tan() const
{
static auto tan_ = [](double d) { return std::tan(d); };
return trig_func(tan_);
}
};
using angle_deg = angle<angle_type::degree>;
using angle_rad = angle<angle_type::radian>;
angle_deg operator"" _deg(long double v)
{
return angle_deg(v);
}
angle_rad operator"" _rad(long double v)
{
return angle_rad(v);
}
template <>
struct to_degree<angle_type::degree>
{
angle_deg operator()(angle_deg d)
{
return d;
}
};
template <>
struct to_degree<angle_type::radian>
{
static constexpr auto rad_to_deg = 180.0 / pi;
angle_deg operator()(angle_rad d)
{
return angle_deg(d.value_ * rad_to_deg);
}
};
template <>
struct convert_a<angle_type::radian>
{
static constexpr auto deg_to_rad = pi / 180.0;
template <angle_type From>
angle_rad operator()(angle<From> a)
{
auto in_deg = to_degree<From>()(a);
return angle_rad(in_deg.value_ * deg_to_rad);
}
};
#endif
Finally, a typesafe cartesian class that puts this all together:
#ifndef UNITS_CARTESIAN_HPP
#define UNITS_CARTESIAN_HPP
#include "angle.hpp"
#include "angle_impl.hpp"
#include "distance.hpp"
#include "quantity.hpp"
#include <ostream>
template <distance D>
struct cartesian
{
private:
quantity<D> x_, y_, z_;
public:
cartesian(quantity<D> x, quantity<D> y, quantity<D> z)
: x_(x),
y_(y),
z_(z)
{ }
quantity<D> x() const { return x_; }
quantity<D> y() const { return y_; }
quantity<D> z() const { return z_; }
cartesian<D> scale(double by) const
{
return cartesian<D>(x_.scale(by), y_.scale(by), z_.scale(by));
}
template <angle_type A>
cartesian<D> rotate_x(angle<A> theta) const
{
const auto rotated_y = y_.scale(theta.cos()) - z_.scale(theta.sin());
const auto rotated_z = y_.scale(theta.sin()) + z_.scale(theta.cos());
return cartesian<D>(x_, rotated_y, rotated_z);
}
template <angle_type A>
cartesian<D> rotate_y(angle<A> theta) const
{
const auto rotated_x = x_.scale(theta.cos()) + z_.scale(theta.sin());
const auto rotated_z = x_.scale(-1.0 * theta.sin()) + z_.scale(theta.cos());
return cartesian<D>(rotated_x, y_, rotated_z);
}
template <angle_type A>
cartesian<D> rotate_z(angle<A> theta) const
{
auto rotated_x = x_.scale(theta.cos()) - y.scale(theta.sin());
auto rotated_y = x_.scale(theta.sin()) * y.scale(theta.cos());
return cartesian<D>(rotated_x, rotated_y, z_);
}
};
using cartesian_m = cartesian<distance::metre>;
using cartesian_km = cartesian<distance::kilometre>;
using cartesian_mi = cartesian<distance::mile>;
template <distance D>
quantity<D> euclid_distance(const cartesian<D>& a, const cartesian<D>& b)
{
auto x_dist = b.x() - a.x();
auto y_dist = b.y() - a.y();
auto z_dist = b.z() - a.z();
return euclid_distance(x_dist, y_dist, z_dist);
}
template <distance D1, distance D2>
cartesian<D1> operator+(const cartesian<D1>& a, const cartesian<D2>& b)
{
return cartesian<D1>(a.x() + b.x(), a.y() + b.y(), a.z() + b.z());
}
template <distance D1, distance D2>
cartesian<D1> operator-(const cartesian<D1>& a, const cartesian<D2>& b)
{
return cartesian<D1>(a.x() - b.x(), a.y() - b.y(), a.z() - b.z());
}
template <distance D>
std::ostream& operator<<(std::ostream& os, const cartesian<D>& c)
{
return os << "Cartesian(" << "x = " << c.x() << ", y = " << c.y()
<< ", z = " << c.z() << ")";
}
#endif
The user is now forced to be explicit when creating a cartesian type what unit it is in:
// Ok, in metres
cartesian_m c_metres{1.0_m, 2.0_m, 3.0_m};
// Ok, in kilometres
cartesian_km c_kilo{1.0_km, 2.0_km, 3.0_km};
// Error, mixed types not allowed
cartesian_m c_mixed{1.0_km, 2.0_m, 3.0_m};
// Addition always converts to the type of the first argument
cartesian_m c_add = c_metres + c_kilo;
// Error if we try and define this as km
cartesian_km c_bad_add = c_metres + c_kilo;
// Ok if we do a type conversion though
cartesian_km c_convert_add = convert<distance::kilometres>()(c_metres) + c_kilo;
// Euclidian distance must be in the same units
cartesian_m c1{1.0_m, 2.0_m, 3.0_m};
cartesian_m c2{5.0_m, 7.0_m, -(10.0_m)};
auto dist = euclid_distance(c1, c2);
std::cout << dist << "\n";
// x-axis rotation by an angle in degrees:
angle_deg d{45};
auto rotated = c7.rotate_x(d);
std::cout << rotated << "\n";
The conversions are (sort of) ugly on purpose; there should be enough flexibility that they shouldn't need to be used too often.
Any feedback welcome - there's a bit of uglyness when it comes to the implementation; quite a number of forward declarations are required, and things are scattered around a bit more than I'd like. I haven't put this in a namespace for now out of sheer lazyness (and because it's prototyping something more than anything else), so I'm aware of that deficiency.