8
\$\begingroup\$

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.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Here's another solution that:

  1. Does not use shared_ptr.
  2. Does not use dynamic memory allocation.
  3. Does not use virtual member functions.
  4. Uses class templates for distance and cartesian.
  5. Defines the units in an extensible manner.

distance.hpp

#ifndef DISTANCE_HPP_
#define DISTANCE_HPP_

#include <cmath>
#include <ostream>

// Functions that must be supported for each unit 
// used to create distance.
template <typename unit> double getScaleFactor();
template <typename unit> std::string getString();

template <typename unit> class distance;

template <typename unit> 
std::ostream& operator<<(std::ostream& out, distance<unit> const& dist);

template <typename unit> 
distance<unit> euclid_distance(distance<unit> const& x,
                               distance<unit> const& y,
                               distance<unit> const& z);

template <typename unit>
class distance
{
   public:

      distance(double d) : d_(d) {}

      distance scale(double by) const
      {
         return distance(d_ * by);
      }

      template <typename another_unit>
         distance<another_unit> convert() const
         {
            // If this object is 1 km and another_unit is m,
            // this function returns 1000 m.

            // Scale factor from this distance to SI unit
            double scaleToSI1 = getScaleFactor<unit>();
            double scaleToSI2 = getScaleFactor<another_unit>();

            double f = scaleToSI1/scaleToSI2;

            return distance<another_unit>(this->d_*f);
         }

      template <typename another_unit>
      distance& operator+=(distance<another_unit> const& rhs)
      {
         distance copy = rhs.convert<unit>();
         d_ += copy.d_;
         return *this;
      }

      template <typename another_unit>
      distance operator+(distance<another_unit> const& rhs) const
      {
         distance ret(*this);
         ret += rhs;
         return ret;
      }

      template <typename another_unit>
      distance& operator-=(distance<another_unit> const& rhs)
      {
         distance copy = rhs.convert<unit>();
         d_ -= copy.d_;
         return *this;
      }

      template <typename another_unit>
      distance operator-(distance<another_unit> const& rhs) const
      {
         distance ret(*this);
         ret -= rhs;
         return ret;
      }

      distance operator-() // Unary - operator.
      {
         return distance(-d_);
      }

      friend std::ostream& operator<< <unit>(std::ostream& out, distance const& dist);

      friend distance euclid_distance<unit>(distance const& x,
                                            distance const& y,
                                            distance const& z);

   private:

      double d_;
};

template <typename unit> 
std::ostream& operator<<(std::ostream& out, distance<unit> const& dist)
{
   return out << dist.d_ << " " << getString<unit>();
}

template <typename unit> 
distance<unit> euclid_distance(distance<unit> const& x,
                               distance<unit> const& y,
                               distance<unit> const& z)
{
   return distance<unit>(std::sqrt(x.d_ * x.d_ + y.d_ * y.d_ + z.d_ * z.d_));
}

#endif

cartesian.hpp

#ifndef CARTESIAN_HPP
#define CARTESIAN_HPP

#include <ostream>

#include "distance.hpp"

template <typename unit>
struct cartesian
{
   private:

      distance<unit> x_;
      distance<unit> y_;
      distance<unit> z_;

   public:

      cartesian(distance<unit> const& x,
                distance<unit> const& y,
                distance<unit> const& z) : x_(x), y_(y), z_(z) { }

      distance<unit> x() const { return x_; }
      distance<unit> y() const { return y_; }
      distance<unit> z() const { return z_; }

      cartesian scale(double by) const
      {
         return cartesian(x_.scale(by), y_.scale(by), z_.scale(by));
      }

      template <typename another_unit>
      cartesian<another_unit> convert() const
      {
         return cartesian<another_unit>(x_.convert<another_unit>(),
                                        y_.convert<another_unit>(),
                                        z_.convert<another_unit>());
      }

};

template <typename unit1, typename unit2>
distance<unit1> euclid_distance(cartesian<unit1> const& a, cartesian<unit2> const& b)
{
   auto x_dist = a.x() - b.x();
   auto y_dist = a.y() - b.y(); 
   auto z_dist = a.z() - b.z();

   return euclid_distance(x_dist, y_dist, z_dist); 
}

template <typename unit1, typename unit2>
cartesian<unit1> operator+(cartesian<unit1> const& a, cartesian<unit2> const& b)
{
   return cartesian<unit1>(a.x() + b.x(), a.y() + b.y(), a.z() + b.z());
}

template <typename unit1, typename unit2>
cartesian<unit1> operator-(cartesian<unit1> const& a, cartesian<unit2> const& b)
{
   return cartesian<unit1>(a.x() - b.x(), a.y() - b.y(), a.z() - b.z());
}

template <typename unit>
std::ostream& operator<<(std::ostream& os, cartesian<unit> const& c)
{
   return os << "Cartesian(" << "x = " << c.x() << ", y = " << c.y()
             << ", z = " << c.z() << ")";
}

#endif

metre.hpp

#ifndef METRE_HPP_
#define METRE_HPP_

#include "distance.hpp"

struct metre;

template <> double getScaleFactor<metre>()
{
   return 1.0;
}

template <> std::string getString<metre>()
{
   return "metres";
}

distance<metre> operator"" _m(long double d)
{
   return distance<metre>(d);
}

#endif

kilometre.hpp

#ifndef KILOMETRE_HPP_
#define KILOMETRE_HPP_

#include "distance.hpp"

struct kilometre;

template <> double getScaleFactor<kilometre>()
{
   return 1000.0;
}

template <> std::string getString<kilometre>()
{
   return "kilometres";
}

distance<kilometre> operator"" _km(long double d)
{
    return distance<kilometre>(d);
}
#endif

mile.hpp

#ifndef MILE_HPP_
#define MILE_HPP_

#include "distance.hpp"

struct mile;

template <> double getScaleFactor<mile>()
{
   return 1609.34;
}

template <> std::string getString<mile>()
{
   return "miles";
}

distance<mile> operator"" _mi(long double d)
{
    return distance<mile>(d);
}

#endif

main.cpp

#include <iostream>

#include "cartesian.hpp"
#include "metre.hpp"
#include "kilometre.hpp"
#include "mile.hpp"

int main()
{
   // Ok, in metres
   cartesian<metre> c_metres{1.0_m, 2.0_m, 3.0_m};

   // Ok, in kilometres
   cartesian<kilometre> c_kilo{1.0_km, 2.0_km, 3.0_km};

   // Mixed types are not allowed
   // cartesian<metre> c_mixed{1.0_km, 2.0_m, 3.0_m};
   // cartesian<metre> c_mixed_2{1.0_km, 2.0_m, 3.0_in};

   // Addition always converts to the type of the first argument
   cartesian<metre> c_add = c_metres + c_kilo;

   // Ok if we do a type conversion though, we get in the converted unit
   cartesian<kilometre> c_convert_add = c_metres.convert<kilometre>() + c_kilo;

   // Euclidian distance can be in mixed units
   cartesian<metre> c1{1.0_m, 2.0_m, 3.0_m};
   cartesian<metre> c2{5.0_m, 7.0_m, -(10.0_m)};
   cartesian<mile> c3{5.0_mi, 7.0_mi, -(10.0_mi)};

   std::cout << euclid_distance(c1, c2) << "\n";
   std::cout << euclid_distance(c1, c3) << "\n";

   return 0;
}

inch.hpp

#ifndef INCH_HPP_
#define INCH_HPP_

#include "distance.hpp"

struct inch;

template <> double getScaleFactor<inch>()
{
   return 0.0254;
}

template <> std::string getString<inch>()
{
   return "inches";
}

distance<inch> operator"" _in(long double d)
{
    return distance<inch>(d);
}

#endif
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks for taking the time to do this. I agree that in terms of extensibility this is much nicer. \$\endgroup\$
    – Yuushi
    Commented Mar 1, 2015 at 4:18
3
\$\begingroup\$

After seeing your code the first time, something didn't feel right. I couldn't quite put my fingers on the problem. However, after going over the code a few times, I came up with a few things that can be changed to improve the code.

Using the right nomenclature

I think the nomenclature is wrong.

Metre, kilometer, and mile are better thought of as units of distance instead of being distances. 12 metres is a distance but metre is not.

Your use of distance and quantity is not quite right. Distance is a quantity + a length unit. When we say distance from point A to point B is 5 kilometres, 5 is the quantity and kilometre is the length unit.

Design of the enum and various classes

It occurred to me that your design violates The Open/Closed Principle.

You have the following enum that's the lynchpin of your entire program.

enum struct distance
{
    metre, kilometre, mile
};

You have class templates with the above enum as template parameters. You have specializations that use the enums. You have user defined suffix operators that depend on the enums.

If you want to add inch to that list, you'll have to search for all the places where these units are used, go through every one of those files, and add another function, another value, another clause that deals with the new unit. These are very intrusive changes, and will most likely introduce bugs.

You shouldn't have to modify existing working code to add new type/enum to the code base.

My suggestion

  1. Create a base class called length_unit. Create sub-types metre, kilometre, and mile. Make sure length_unit has the necessary virtual member functions that are needed for supporting the high level functionality.

  2. Implement metre, kilometre, and mile in separate files. When the need for adding inch comes, it will be a very simple process.

  3. Create a class distance that captures the quantity part and the length unit part.

  4. Remove quantity altogether.

  5. Make cartesian a regular class, not a class template.

P.S. I have left out the code dealing with angles since they can be updated in a manner similar to distance.

Here are the files and their contents:

length_unit.hpp

#ifndef LENGTH_UNIT_HPP_
#define LENGTH_UNIT_HPP_

#include <ostream>

struct length_unit
{
   virtual ~length_unit() {}

   virtual length_unit* copy() const = 0;

   virtual double getScaleFactor() const = 0;  // Scale factor to SI unit.

   virtual std::ostream& write(std::ostream& out) const = 0;
};

inline std::ostream& operator<<(std::ostream& out, length_unit const& unit)
{
   return unit.write(out);
}

#endif

distance.hpp

#ifndef DISTANCE_HPP_
#define DISTANCE_HPP_

#include <ostream>
#include <memory>

#include "length_unit.hpp"

class distance
{
   public:

      distance(double d, length_unit const& unit)
         : d_(d), unit_ptr_(unit.copy())
      { }

      distance(double d, std::shared_ptr<length_unit> const& unit_ptr)
         : d_(d), unit_ptr_(unit_ptr)
      { }

      distance scale(double by) const
      {
         return distance(d_ * by, unit_ptr_);
      }

      distance convert(length_unit const& unit) const;

      template <typename unit> distance convert() const
      {
         return this->convert(unit());
      }

      distance& operator+=(distance const& rhs);
      distance operator+(distance const& rhs) const;

      distance& operator-=(distance const& rhs);
      distance operator-(distance const& rhs) const;
      distance operator-(); // Unary - operator.

      friend std::ostream& operator<<(std::ostream& out, distance const& dist);

      friend distance euclid_distance(distance const& x, distance const& y, distance const& z);

   private:

      double d_;
      std::shared_ptr<length_unit> unit_ptr_;
};

#endif

distance.cpp

#include <cmath>
#include "distance.hpp"

distance distance::convert(length_unit const& unit) const
{
   // Scale factor from this distance to SI unit
   double scaleToSI1 = unit_ptr_->getScaleFactor();
   double scaleToSI2 = unit.getScaleFactor();

   double f = scaleToSI1/scaleToSI2;

   return distance(this->d_*f, std::shared_ptr<length_unit>(unit.copy()));
}

distance& distance::operator+=(distance const& rhs)
{
   distance copy = rhs.convert(*(this->unit_ptr_));
   d_ += copy.d_;
   return *this;
}

distance distance::operator+(distance const& rhs) const
{
   distance ret(*this);
   ret += rhs;
   return ret;
}

distance& distance::operator-=(distance const& rhs)
{
   distance copy = rhs.convert(*(this->unit_ptr_));
   d_ -= copy.d_;
   return *this;
}

distance distance::operator-(distance const& rhs) const
{
   distance ret(*this);
   ret -= rhs;
   return ret;
}

distance distance::operator-()
{
   return distance(-d_, unit_ptr_);
}

std::ostream& operator<<(std::ostream& out, distance const& dist)
{
   return out << dist.d_ << " " << *(dist.unit_ptr_);
}

distance euclid_distance(distance const& x, distance const& y, distance const& z)
{
   distance y_copy = y.convert(*(x.unit_ptr_));
   distance z_copy = z.convert(*(x.unit_ptr_));

   return distance(std::sqrt(x.d_ * x.d_ + y_copy.d_ * y_copy.d_ + z_copy.d_ * z_copy.d_), x.unit_ptr_);
}

Notice that distance does not know anything about specific types of units. It is able to implement its functionality without knowing about them.

cartesian.hpp

#ifndef CARTESIAN_HPP
#define CARTESIAN_HPP

#include <ostream>

#include "distance.hpp"

struct cartesian
{
   private:

      distance x_, y_, z_;

   public:

      cartesian(distance const& x, distance const& y, distance const& z)
         : x_(x),
         y_(y),
         z_(z)
      { }

      distance x() const { return x_; }
      distance y() const { return y_; }
      distance z() const { return z_; }

      cartesian scale(double by) const
      {
         return cartesian(x_.scale(by), y_.scale(by), z_.scale(by));
      }

      template <typename unit>
      cartesian convert() const
      {
         return cartesian(x_.convert<unit>(), y_.convert<unit>(), z_.convert<unit>());
      }

};

extern distance euclid_distance(cartesian const& a, cartesian const& b);

extern cartesian operator+(cartesian const& a, cartesian const& b);

extern cartesian operator-(cartesian const& a, cartesian const& b);

extern std::ostream& operator<<(std::ostream& os, cartesian const& c);

#endif

cartesian.cpp

#include "cartesian.hpp"

distance euclid_distance(cartesian const& a, cartesian const& 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); 
}

cartesian operator+(cartesian const& a, cartesian const& b)
{
    return cartesian(a.x() + b.x(), a.y() + b.y(), a.z() + b.z());
}

cartesian operator-(cartesian const& a, cartesian const& b)
{
    return cartesian(a.x() - b.x(), a.y() - b.y(), a.z() - b.z());
}

std::ostream& operator<<(std::ostream& os, cartesian const& c)
{
    return os << "Cartesian(" << "x = " << c.x() << ", y = " << c.y()
              << ", z = " << c.z() << ")";
}

length_unit_impl.hpp

This file has couple of helper function templates and a class template to make implementation of concrete length_units as painless as possible.

#ifndef LENGTH_UNIT_IMPL_HPP_
#define LENGTH_UNIT_IMPL_HPP_

#include <string>

#include "length_unit.hpp"

template <typename unit> double getScaleFactor();
template <typename unit> std::string getString();

template <typename unit>
struct length_unit_impl : length_unit
{
   virtual ~length_unit_impl() {}

   virtual length_unit* copy() const
   {
      return new length_unit_impl();
   }

   virtual double getScaleFactor() const
   {
      return ::getScaleFactor<unit>();
   }

   virtual std::ostream& write(std::ostream& out) const
   {
      return out << ::getString<unit>();
   }
};

#endif

metre.hpp

#ifndef METRE_HPP_
#define METRE_HPP_

#include "distance.hpp"
#include "length_unit_impl.hpp"

struct metre_t;

template <> double getScaleFactor<metre_t>()
{
   return 1.0;
}

template <> std::string getString<metre_t>()
{
   return "metre";
}

typedef length_unit_impl<metre_t> metre;

distance operator"" _m(long double d)
{
   return distance(d, metre());
}

#endif

kilometre.hpp

#ifndef KILOMETRE_HPP_
#define KILOMETRE_HPP_

#include "distance.hpp"
#include "length_unit_impl.hpp"

struct kilometre_t;

template <> double getScaleFactor<kilometre_t>()
{
   return 1000.0;
}

template <> std::string getString<kilometre_t>()
{
   return "kilometres";
}

typedef length_unit_impl<kilometre_t> kilometre;

distance operator"" _km(long double d)
{
    return distance(d, kilometre());
}
#endif

mile.hpp

#ifndef MILE_HPP_
#define MILE_HPP_

#include "distance.hpp"
#include "length_unit_impl.hpp"

struct mile_t;

template <> double getScaleFactor<mile_t>()
{
   return 1609.34;
}

template <> std::string getString<mile_t>()
{
   return "miles";
}

typedef length_unit_impl<mile_t> mile;

distance operator"" _mi(long double d)
{
    return distance(d, mile());
}

#endif

main.cpp

A test program.

#include <iostream>

#include "cartesian.hpp"
#include "metre.hpp"
#include "kilometre.hpp"
#include "mile.hpp"

int main()
{
   // Ok, in metres
   cartesian c_metres{1.0_m, 2.0_m, 3.0_m};

   // Ok, in kilometres
   cartesian c_kilo{1.0_km, 2.0_km, 3.0_km};

   // OK, mixed types are allowed
   cartesian c_mixed{1.0_km, 2.0_m, 3.0_m};

   // Addition always converts to the type of the first argument
   cartesian c_add = c_metres + c_kilo;

   // Ok if we do a type conversion though, we get in the converted unit
   cartesian c_convert_add = c_metres.convert<kilometre>() + c_kilo;

   // Euclidian distance can be in mixed units
   cartesian c1{1.0_m, 2.0_m, 3.0_m};
   cartesian c2{5.0_m, 7.0_m, -(0.05_mi)};

   auto dist = euclid_distance(c1, c2);
   std::cout << dist << "\n";

   return 0;
}

Adding inch as a unit

When the time is ready for adding inch as a unit, all you need to do is create a file with the following contents:

inch.hpp

#ifndef INCH_HPP_
#define INCH_HPP_

#include "distance.hpp"
#include "length_unit_impl.hpp"

struct inch_t;

template <> double getScaleFactor<inch_t>()
{
   return 0.0254;
}

template <> std::string getString<inch_t>()
{
   return "inches";
}

typedef length_unit_impl<inch_t> inch;

distance operator"" _in(long double d)
{
    return distance(d, inch());
}

#endif

and now, you can start using inches in your program.

cartesian c_mixed{1.0_km, 2.0_m, 3.0_in};
\$\endgroup\$
2
  • \$\begingroup\$ I don't disagree with your comments necessarily, but this loses one very important property: low run-time overhead. Your code requires dynamic memory allocations, the overhead of shared_ptr, and virtual calls. One of the big ideas behind this (that I guess I should have mentioned) is that it should have 0 runtime overhead with a sprinkling of constexpr in the right places, at least for values known at compile time. \$\endgroup\$
    – Yuushi
    Commented Feb 28, 2015 at 7:29
  • \$\begingroup\$ I agree that the nomenclature is quite sloppy. That is definitely something I should fix. \$\endgroup\$
    – Yuushi
    Commented Feb 28, 2015 at 7:30

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.