3
\$\begingroup\$

I'm writing a small C++14 library for myself that allows me to decorate a type with dimensionality information, so that I can leverage the template/type system to avoid mistakes such as adding velocity to distance (or similar). This is my first time working with C++ templates to any meaningful extent. I know that this reinvents the wheel, but I'm trying to focus more on improving my skills with C++ and C++ template programming.

In my approach, I'm wrapping some type (let's call it TYPE) in an instantiation of a class template (dim). This template includes other template arguments (LEN, MASS, TIME), each of which corresponds to the power of that fundamental unit. For example, a 64-bit floating point acceleration (length*time-2) would be a dim<double, 1, 0, -2>.

I don't want to deal with the complexity of wrapping a vector/collection type and implementing logic to wrap and unwrap dimensional values when indexing into the collection, so I expect that a vector of masses would be a vector<dim<double, 0, 1, 0>>, not a dim<vector<double>, 0, 1, 0>

My goals are to keep the code memory- and time-efficient at runtime when compiled with optimization enabled, while keeping it fairly convenient to use. So far, inspections of assembly make me believe that this is the case (the class is only as large the type it wraps, and the assembly looks terse and clean).

Eventually, I would like to include some dimensionally-aware specializations of helpers such as square root, power, etc, as well as other underlying types used for the dimensional value (so I could have e.g. a dimensioned value including experimental uncertainty i.e. dim<uncertain<double>, 1, 1, -2)

The class definition is below. Please note that I've removed some repetitive operator overloads (e.g. subtraction and division, because they are analogous to addition and multiplication)

#ifndef DIMENSIONAL_DIMENSIONAL_H
#define DIMENSIONAL_DIMENSIONAL_H

#include <type_traits>
#include <ostream>

template<class TYPE, int LEN, int MASS, int TIME>
class dim {
    TYPE value_;
public:
    dim(const TYPE value) : value_(value) {} // construct explicitly

    // copy, allowing conversion of contained type
    // Unfortunately, this doesn't warn properly for narrowing conversions.
    template<class TYPE2> explicit dim(const dim<TYPE2, LEN, MASS, TIME> &d) {
        value_ = d.value();
    }
    dim(dim<TYPE, LEN, MASS, TIME> const &d) {
        value_ = d.value();
    }

    // comparison
    // it's possible to make this a non-template function(dim<TYPE, LEN, MASS, TIME> &rhs)
    // However, template and static_assert gives better diagnostics to the user.
    template<class TYPE2, int LEN2, int MASS2, int TIME2>
    bool operator==(const dim<TYPE2, LEN2, MASS2, TIME2> &rhs) const {
        static_assert(std::is_same<TYPE, TYPE2>(), "Integral type of dimensional expressions must match");
        static_assert(LEN == LEN2 && MASS == MASS2 && TIME == TIME2,
                      "Dimension types of dimensional expressions must match");
        return value_ == rhs.value_;
    }

    bool operator!=(const dim &rhs) const {
        return !(rhs == *this);
    }

    template<class TYPE2, int LEN2, int MASS2, int TIME2>
    bool operator<(const dim<TYPE2, LEN2, MASS2, TIME2> &rhs) const {
        static_assert(LEN == LEN2 && MASS == MASS2 && TIME == TIME2,
                      "Dimension types of dimensional expressions must match");
        return value_ < rhs.value_;
    }

    bool operator>(const dim &rhs) const {

        return rhs < *this;
    }

    bool operator<=(const dim &rhs) const {
        return !(rhs < *this);
    }

    bool operator>=(const dim &rhs) const {
        return !(*this < rhs);
    }

    // arithmetic
    template<class TYPE2, int LEN2, int MASS2, int TIME2, typename RTYPE=typename std::common_type<TYPE, TYPE2>::type>
    auto operator+(const dim<TYPE2, LEN2, MASS2, TIME2> &rhs) const {
        static_assert(LEN == LEN2 && MASS == MASS2 && TIME == TIME2,
                      "Dimension types of dimensional expressions must match");
        return dim<RTYPE, LEN, MASS, TIME>(value() + rhs.value());
    }

    template<class TYPE2, int LEN2, int MASS2, int TIME2>
    auto operator+=(const dim<TYPE2, LEN2, MASS2, TIME2> &rhs) {
        static_assert(LEN == LEN2 && MASS == MASS2 && TIME == TIME2,
                      "Dimension types of dimensional expressions must match");
        value_ += rhs.value();
        return *this;
    }

    // subtraction implemented similarly

    template<class TYPE2, int LEN2, int MASS2, int TIME2, typename RTYPE=typename std::common_type<TYPE, TYPE2>::type>
    auto operator*(const dim<TYPE2, LEN2, MASS2, TIME2> &rhs) const {
        return dim<RTYPE, LEN+LEN2, MASS+MASS2, TIME+TIME2>(value() * rhs.value());
    }
    template<class TYPE2, typename RTYPE=typename std::common_type<TYPE, TYPE2>::type>
    auto operator*(const TYPE2 &rhs) const {
        return dim<RTYPE, LEN, MASS, TIME>(value() * rhs);
    }
    template<class TYPE2, int LEN2, int MASS2, int TIME2>
    auto operator*=(const dim<TYPE2, LEN2, MASS2, TIME2> &rhs) {
        static_assert(LEN2 == 0 && MASS2 == 0 && TIME2 == 0,
                      "Compound multiplication only defined for unitless right-hand-side");
        value_ *= rhs.value();
        return *this;
    }
    auto operator*=(const TYPE &rhs) {
        value_ *= rhs;
        return *this;
    }

    // division implemented similarly

    // mutation
    void set(TYPE value) {
        value_ = value;
    }
    template<class TYPE2, int LEN2, int MASS2, int TIME2>
    dim<TYPE, LEN, MASS, TIME>& operator=(const dim<TYPE2, LEN2, MASS2, TIME2> &rhs) {
        static_assert(LEN == LEN2 && MASS == MASS2 && TIME == TIME2,
                      "Dimension types of dimensional expressions must match");
        value_ = rhs.value();
        return *this;
    }

    dim<TYPE, LEN, MASS, TIME>& operator=(TYPE rhs) {
        value_ = rhs;
        return *this;
    }

    // conversion
    TYPE value() const {
        return value_;
    }

    operator TYPE() const {
        static_assert(LEN == 0 && MASS == 0 && TIME == 0, "Conversion operator only defined for dimensionless values.");
        return value_;
    }

    friend std::ostream &operator<<(std::ostream &os, const dim &d) {
        // Snip; prints e.g. "9.81 [m^1 s^-2]"
    }
};

template<typename T> dim<T, 0, 0, 0> make_pure(T t) {
    return dim<T, 0, 0, 0>(t);
}


#endif //DIMENSIONAL_DIMENSIONAL_H

Here's an example of how I would use it:

auto mass = dim<double, 0, 1, 0>(10);
auto dist = dim<double, 1, 0, 0>(15);
auto time = dim<double, 0, 0, 1>(5);
auto momentum = mass*dist/time;
std::cout << "momentum: " << momentum << std::endl;

Of course, in a larger project, I could use type aliases for useful derived units.

I'm currently facing some difficulties with the special-case of unitless values (i.e. dim<TYPE, 0, 0, 0>). The multiplication and division operator overloads are fairly bloated since they special-case the multiplication by a "normal" numeric type. I'm wondering if it's worth it to implicitly convert e.g. double into dim<double, 0, 0, 0>, to avoid this bloat and facilitate cleaner usage.

I'm also facing the issue of extending the set of base units to include other base units (bits, kelvin, etc). The current design isn't terribly extensible and requires additional template arguments added everywhere whenever a new base unit is added.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Well, instead of reinventing the well, I have done a search and found:

From those links, you can find other links like units from boost.

Some library are more complete than others and will allows to do unit conversions for example.

By the way, C++ already contains units for duration (seconds, minutes, hours…)

As you have noticed, your approach don't scale well when adding other unit types and is relatively limited since you only support some unspecified unit for a given measurement (are you using SI or US units?).

Finally some of your operator seems to return incorrect type like operator * which seems to ignore LEN2, MASS2... OK This one is used when multiplying by a number.

  • In the constructor dim(const TYPE value) there is no point to specify const. In fact, you might want to prevent TYPE from being a reference.
  • You might consider making that constructor explicit to prevent unwanted conversion.
  • For * operator, you might want to provide an operator where the left hand-side is a number. It would need to be a free function. Because of commutativity, you could simply call existing member function that take a number on the right side.
  • For your comparison operators, you don't handle right-hand side the same way for all operators if left and right operator are not of the same type. For == and < you will get a static assertion if the dimensions are different while for others (!=, >, <=, >=), the function would not be defined (and the conversion constructor would not be used because it is explicit).
\$\endgroup\$
2
  • \$\begingroup\$ Thank you for the feedback and the references on other implementations. In spite of the other solutions I'd still like to finish reinventing this wheel for the practice with the language. That said, I'm struggling to see the error on operator*'s overloads since I'm not fully comfortable with the template logic involved; as far as I can tell I am properly adding the base unit exponents in the operator*(dim... overload and handling a unitless scalar as a unitless scalar in the operator*(TYPE2 version. Would you be able to clarify the issue there? \$\endgroup\$
    – nanofarad
    Commented Jan 4, 2019 at 2:36
  • 1
    \$\begingroup\$ Nevertheless, there is no point for us to repeat what others have already done. At that point, you know some limitation of your approach like hard to add new units so most improvement will come by learning what other have done. Once read, you could try to do it by yourself maybe a few days later without using the reference. \$\endgroup\$
    – Phil1970
    Commented Jan 4, 2019 at 14:14

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.