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.