patternpythonMinor
Typesafe Unit Code Generation
Viewed 0 times
codegenerationtypesafeunit
Problem
I've previously posted some code that deals with making Cartesian co-ordinates safe at compile time.
I've been doing a bit more thinking about this in a more general setting, as it is a problem that I quite consistently run into in code bases (all quantities are just declared as
Given that a large number of unit conversions require only multiplication/division by some constant factor, theoretically there should be a lot of similarity between code that deals with making various unit systems type safe.
To this end, I've written something in Python that can generate C++ implementations for any unit types that have simple conversions between each other. First, an example of the usage.
This will generate the following C++11/14 code:
```
#ifndef FREQUENCY_AUTOGENERATED_HPP_INCLUDED_
#define FREQUENCY_AUTOGENERATED_HPP_INCLUDED_
#include
#include
namespace unit
{
template
class frequency
{
public:
constexpr explicit frequency(double v)
: value_(v)
{ }
constexpr double value() const
{
return value_;
}
private:
double value_;
};
struct Hz
{
static std::string to_string()
{
return {"Hz"};
}
constexpr static frequency from_hz(double v)
{
return frequency(v);
}
constexpr static frequency to_hz(double v)
{
return frequency(v);
}
};
template
struct basic_convert
{
const static double factor;
constexpr static frequency from_hz(double v)
{
return frequency(v / factor);
}
co
I've been doing a bit more thinking about this in a more general setting, as it is a problem that I quite consistently run into in code bases (all quantities are just declared as
double and then forgotten about; or at best given a small comment about what unit it is supposed to be in). Making the unit an explicit part of the type, and using the type system to enforce this, is something I think is of great benefit.Given that a large number of unit conversions require only multiplication/division by some constant factor, theoretically there should be a lot of similarity between code that deals with making various unit systems type safe.
To this end, I've written something in Python that can generate C++ implementations for any unit types that have simple conversions between each other. First, an example of the usage.
u = UnitGenerator('frequency', 'Hz', ['kHz', 'MHz', 'GHz'])
u.add_conversion('kHz', 1e3)
u.add_conversion('MHz', 1e6)
u.add_conversion('GHz', 1e9)
u.has_plus_operator()
u.has_minus_operator()
u.generate('frequency.hpp')This will generate the following C++11/14 code:
```
#ifndef FREQUENCY_AUTOGENERATED_HPP_INCLUDED_
#define FREQUENCY_AUTOGENERATED_HPP_INCLUDED_
#include
#include
namespace unit
{
template
class frequency
{
public:
constexpr explicit frequency(double v)
: value_(v)
{ }
constexpr double value() const
{
return value_;
}
private:
double value_;
};
struct Hz
{
static std::string to_string()
{
return {"Hz"};
}
constexpr static frequency from_hz(double v)
{
return frequency(v);
}
constexpr static frequency to_hz(double v)
{
return frequency(v);
}
};
template
struct basic_convert
{
const static double factor;
constexpr static frequency from_hz(double v)
{
return frequency(v / factor);
}
co
Solution
Why Code Generation?
This is my main question. Why? What advantage does code generation give you over just writing the class templates in C++ directly? I actually don't think it gives you any - you just end up with this added layer of complexity for where the code comes from.
Furthermore, the generated code isn't particularly easy to follow. We have
This is my main question. Why? What advantage does code generation give you over just writing the class templates in C++ directly? I actually don't think it gives you any - you just end up with this added layer of complexity for where the code comes from.
Furthermore, the generated code isn't particularly easy to follow. We have
Hz, kHz, MHz, and GHz. But where those differ is a static constant that is separately defined. That works functionally, but is confusing. Consider an example from `:
std::chrono::nanoseconds duration
std::chrono::microseconds duration
std::chrono::milliseconds duration
std::chrono::seconds duration
std::chrono::minutes duration>
std::chrono::hours duration>
That seems like a much better model. What if we just defined:
using Hz = frequency<>;
using MHz = frequency;
using GHz = frequency;
There are two things we lose with this approach w.r.t. code generation:
- Easy naming of types
- Easy addition of user-defined literals.
However, we'd gain the ability to write:
auto freq = GHz{40};
It's worth mentioning that making the basic_convert destructor private is a very good design choice, since your GHz is just a tag. But I'd rather be able to construct my GHz directly!
Also, it's much easier to write comments in hand-written code than in computer-written code.
Code Duplication
Let's say we went ahead and setup frequency and angle and weight and distance. We now have 4 different function templates for each operator. Now this is all correct, for the sense that your code will not allow me to do stupid things like adding kg and deg. But then... we have four.
Consider a different sort of tagging system:
template >
struct Unit { ... };
This would let us define:
struct frequency_tag;
struct angle_tag;
struct weight_tag;
using Hz = Unit;
using kg = Unit;
using rad = Unit>; // or write your own ratio class
This lets you write one single operator+ across all of your unit types:
template
???? operator+(Unit lhs, Unit rhs)
{
...
}
Also, this approach sets you down the path of being able to actually implement multiplication of units. How would you do that with your code generation approach?
Your Code Specifically
Minor things I wanted to mention here too. Looking at this from a user perspective:
u = UnitGenerator('frequency', 'Hz', ['kHz', 'MHz', 'GHz'])
u.add_conversion('kHz', 1e3)
u.add_conversion('MHz', 1e6)
u.add_conversion('GHz', 1e9)
u.has_plus_operator()
u.has_minus_operator()
Is it really reasonable to support addition but not subtraction? It may not make sense to have negative units in some cases, but doesn't it always makes sense to subtract when you can add? I'd suggest:
u.has_plusminus_operator()
Also, while you have error checking in case the user forgets to add a conversion (good!), I'd suggest making it more direct. Either all during construction:
u = UnitGenerator('frequency', 'Hz',
[('kHz', 1e3), ('MHz', 1e6), ('GHz', 1e9)])
or just all during add_conversion:
u = UnitGenerator('frequency', 'Hz')
u.add_conversion('kHz', 1e3)
u.add_conversion('MHz', 1e6)
u.add_conversion('GHz', 1e9)
That way you don't need error-checking, since there's no way to do it wrong.
One thing you don't error check is the unit name. Should make sure I can't even construct something like:
u = UnitGenerator('1frequency', '3Chainz')
And lastly, consider a fluency model. Have each function return self so that you can write:
(UnitGenerator('time', 's')
.add_conversion('ms', 1e3)
.add_conversion('us', 1e6)
.add_conversion('ns', 1e9)
.has_plusminus_operator()
.generate('time.hpp'))
I don't know how "pythonic" this is - but at least we don't have u`!Code Snippets
std::chrono::nanoseconds duration</*signed integer*/, std::nano>
std::chrono::microseconds duration</*signed integer*/, std::micro>
std::chrono::milliseconds duration</*signed integer*/, std::milli>
std::chrono::seconds duration</*signed integer*/>
std::chrono::minutes duration</*signed integer*/, std::ratio<60>>
std::chrono::hours duration</*signed integer*/, std::ratio<3600>>using Hz = frequency<>;
using MHz = frequency<std::mega>;
using GHz = frequency<std::giga>;auto freq = GHz{40};template <typename tag, typename Value, typename Ratio = std::ratio<1>>
struct Unit { ... };struct frequency_tag;
struct angle_tag;
struct weight_tag;
using Hz = Unit<frequency_tag, double>;
using kg = Unit<weight_tag, double, std::kilo>;
using rad = Unit<angle_tag, double, std::ratio<104348,5978700>>; // or write your own ratio classContext
StackExchange Code Review Q#107643, answer score: 3
Revisions (0)
No revisions yet.