4
\$\begingroup\$

I wrote a small type-rich MKS Unit system for the consistent and safe calculation of physical units in everyday use.

I realized some operators' implementations via the Barton-Nackman trick while defining types via an unique template parameter fixed upon object construction. This prevents e.g. the addition of inconsistent units etc.

#include <string>
#include <sstream>

template<typename Value>
struct OperatorFacade {
  friend constexpr bool operator!=(Value const &lhs, Value const &rhs)
  noexcept {
    return !(lhs==rhs);
  }
  friend constexpr bool operator>(Value const &lhs, Value const &rhs) noexcept {
    return rhs < lhs;
  }
  friend constexpr bool operator<=(Value const &lhs, Value const &rhs)
  noexcept {
    return !(rhs > lhs);
  }
  friend constexpr bool operator>=(Value const &lhs, Value const &rhs)
  noexcept {
    return !(rhs < lhs);
  }
  friend constexpr auto &operator<<(std::ostream &os, Value const other)
  noexcept {
    return os << static_cast<long double>(other);
  }
  friend constexpr auto operator-(Value const &lhs,
                                  Value const &rhs) noexcept {
    return Value{lhs} -= rhs;
  }
  friend constexpr auto operator+(Value const &lhs,
                                  Value const &rhs) noexcept {
    return Value{lhs} += rhs;
  }
};

// Type-safety at compile-time
template<int M = 0, int K = 0, int S = 0>
struct MksUnit {
  enum { metre = M, kilogram = K, second = S };
};

template<typename U = MksUnit<>> // default to dimensionless value
class Value final : public OperatorFacade<Value<U>> {
 public:
  constexpr explicit Value() noexcept = default;
  constexpr explicit Value(long double magnitude) noexcept
      : magnitude_{magnitude} {}
  //constexpr auto &magnitude()  noexcept { return magnitude_; }
  constexpr explicit operator long double() const noexcept {
    return
        magnitude_;
  }

  friend bool operator==(Value const &lhs, Value const &rhs) {
    return static_cast<long double>(lhs)==static_cast<long double>(rhs);
  }
  friend bool operator<(Value const &lhs, Value const &rhs) {
    return static_cast<long double>(lhs) < static_cast<long double>(rhs);
  }

  auto &operator+=(Value const &other) {
    magnitude_ += static_cast<long double>(other);
    return *this;
  }
  auto &operator-=(Value const &other) {
    magnitude_ -= static_cast<long double>(other);
    return *this;
  }
  auto const &operator*(long double scalar) const {
    magnitude_ *= scalar;
    return *this;
  }
  friend auto &operator*(long double scalar, Value const &other) {
    return other.operator*(scalar);
  }

 private:
  long double mutable magnitude_{0.0};
};

// Some handy alias declarations
using DimensionlessQuantity = Value<>;
using Length = Value<MksUnit<1, 0, 0>>;
using Area = Value<MksUnit<2, 0, 0>>;
using Volume = Value<MksUnit<3, 0, 0>>;
using Mass = Value<MksUnit<0, 1, 0>>;
using Time = Value<MksUnit<0, 0, 1>>;
using Velocity = Value<MksUnit<1, 0, -1>>;
using Acceleration = Value<MksUnit<1, 0, -2>>;
using Frequency = Value<MksUnit<0, 0, -1>>;
using Force = Value<MksUnit<1, 1, -2>>;
using Pressure = Value<MksUnit<-1, 1, -2>>;
using Momentum = Value<MksUnit<1, 1, -1>>;

// A couple of convenient factory functions
constexpr auto operator "" _N(long double magnitude) {
  return Force{magnitude};
}
constexpr auto operator "" _ms2(long double magnitude) {
  return Acceleration{magnitude};
}
constexpr auto operator "" _s(long double magnitude) {
  return Time{magnitude};
}
constexpr auto operator "" _Ns(long double magnitude) {
  return Momentum{magnitude};
}
constexpr auto operator "" _m(long double magnitude) {
  return Length{magnitude};
}
constexpr auto operator "" _ms(long double magnitude) {
  return Velocity{magnitude};
}
constexpr auto operator "" _kg(long double magnitude) {
  return Mass{magnitude};
}
constexpr auto operator "" _1s(long double magnitude) {
  return Frequency{magnitude};
}

// Arithmetic operators for consistent type-rich conversions of SI-Units
template<int M1, int K1, int S1, int M2, int K2, int S2>
constexpr auto operator*(Value<MksUnit<M1, K1, S1>> const &lhs,
                         Value<MksUnit<M2, K2, S2>> const &rhs) noexcept {
  return Value<MksUnit<M1 + M2, K1 + K2, S1 + S2>>{
      static_cast<long double>(lhs)*static_cast<long double>(rhs)};
}

template<int M1, int K1, int S1, int M2, int K2, int S2>
constexpr auto operator/(Value<MksUnit<M1, K1, S1>> const &lhs,
                         Value<MksUnit<M2, K2, S2>> const &rhs) noexcept {
  return Value<MksUnit<M1 - M2, K1 - K2, S1 - S2>>{
      static_cast<long double>(lhs)/static_cast<long double>(rhs)};
}

// Scientific constants
auto constexpr speedOfLight = 299792458.0_ms;
auto constexpr gravitationalAccelerationOnEarth = 9.80665_ms2;

void applyMomentumToSpacecraftBody(Momentum const &impulseValue) {};


int main(){
std::cout << "Consistent? " << 10.0_ms - 5.0_m << std::endl;
}

Do you mind taking a look and tell me what you think and where I can improve?

\$\endgroup\$
  • 1
    \$\begingroup\$ No candela or ampere? :-( \$\endgroup\$ – Toby Speight Aug 20 '18 at 12:34
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ – hoffmale Aug 20 '18 at 14:10
4
\$\begingroup\$

Value issues

  • friend bool operator==(const Value& lhs, const Value& rhs) can be noexcept. Also, why use those static_casts instead of simply comparing lhs.magnitude == rhs.magnitude? That's why it's a friend in the first place: To allow access to non-public members.

  • Similar for operator<.

  • operator+= and operator-=: Both can be noexcept, and in both the static_cast can be replaced by accessing other.magnitude.

  • auto const &operator*(long double scalar) const

    Just that signature gives me a headache. A multiplication is supposed to return a new value, not modify one of its operands! If I do c = b * a; (and a != 1), I wouldn't expect b == c afterwards.

    So let's drop the const & part of the return type, and change the function body to return a new Value with the adjusted magnitude:

    auto operator*(long double scalar) const noexcept {
        return Value{ _magnitude * scalar };
    }
    
  • Similar for friend auto& operator*(long double scalar, Value const& other): drop the reference from the return type.

  • long double mutable magnitude_{0.0};: Why does this need to be mutable (other than to make the "wrong" scalar multiplication work)?

General stuff

  • Please put the user-defined literals into their own namespace. This allows user to choose which literals should apply. I can already see collisions with literals from the <chrono> header!

  • Is there a reason long double gets passed by value. but Value isn't? They should be the same size, after all.

  • As @TobySpeigh mentioned in a comment, there are other SI base units, like candela or Ampere. It's surprising that those are missing.

  • Also, is there a reason for using kilograms instead of grams as the base unit? That factor of 1000 shouldn't make that much of a difference.

  • operators *=, /=, % and %= are missing throughout the implementation.

  • With some effort, most if not all of this library could be made constexpr, thus allowing better optimization (or precalculation of values at compile time).

  • Some of those "values" not only have a magnitude, but also a direction (i.e. they're a vector, not a scalar). The current system can't really handle those.

\$\endgroup\$
  • \$\begingroup\$ thank you for your valuable and detailed input. I have fixed all the issues you mentioned and updated the code above. Regarding the general stuff section. Most of those points are still on the agenda and will follow as I see fit. \$\endgroup\$ – Carsten D Aug 20 '18 at 13:46
  • \$\begingroup\$ revised version can be found here \$\endgroup\$ – Carsten D Aug 20 '18 at 14:16
  • \$\begingroup\$ There's a good real-world case for using kg as a base unit, as that's what SI does. Doing otherwise would mean scaling many physical constants from the units they are normally expressed in, and being unable to write derived units as simple typedefs. \$\endgroup\$ – Toby Speight Aug 20 '18 at 15:46
  • \$\begingroup\$ @hoffmale: The reason why I didnt include compound multiplication is, that every multiplication yields a novel physical unit, so a compound expression in this context is rather confusing \$\endgroup\$ – Carsten D Aug 20 '18 at 15:56
  • \$\begingroup\$ @CarstenD: You can still use compound multiplication, division and modulo with scalars. \$\endgroup\$ – hoffmale Aug 20 '18 at 15:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.