nholthaus / units

a compile-time, header-only, dimensional analysis and unit conversion library built on c++14 with no dependencies.
http://nholthaus.github.io/units/
MIT License
952 stars 135 forks source link

How to write template functions with templateized `units` parameters? #97

Closed GElliott closed 6 years ago

GElliott commented 6 years ago

Here is a common pattern for defining a template function that accepts std::chrono::duration types:

template <typename Rep, typename Ratio>
void set_duration(std::chrono::duration<Rep, Ratio> duration) { ... }

I would like to do the same thing with units types. It's not clear to me how this should be accomplished. This definition partially works:

template <typename Rep, typename Ratio>
void set_frequency(units::unit_t<units::unit<Ratio, units::category::frequency_unit>, Rep> frequency) { ... }

I can pass in units::frequency::hertz_t types to this function. However, I get a compile-time error when I try to pass in units::frequency::kilohertz_t (clang, Linux, 3.8):

note: candidate template ignored: could not match 'units::base_unit<std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<-1, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1> >' against 'units::unit<std::ratio<1, 1>, units::base_unit<std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<-1, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1> >, std::ratio<0, 1>, std::ratio<0, 1> >'

How should I define the templatized parameter frequency?

nholthaus commented 6 years ago

It's a bit different of a way to think about the problem, but this is what I prefer for your use case is to enforce the concept with a static assert:

template<class FreqUnitType>
void set_frequency(FreqUnitType frequency)
{
    static_assert(units::traits::is_frequency_unit<FreqUnitType>::value, 
        "FreqUnitType must be a unit_t type representing a frequency value");
    // ...
}

Each unit category has an associated trait. You could also use std::is_convertible_v<FreqUnitType, units::frequency::hertz_t>.

This has the added benefit of (IMO) being more readable and obvious to what the requirement is, as well as providing a logical and succinct error message on misuse.

Another simple method is to parameterize on hertz_t. Since all frequency units are implicitly convertible, you can mix and match them without worry:

void set_frequency(units::frequency::hertz_t frequency) { //... }

myClass.set_frequency(100_kHz);  // this works fine
myClass.set_frequency(10_m);      // compile error

In most situations it's not even more expensive because you could very well do a conversion to store the frequency in a member, or whatever it is you do with it.

In cases where you want to overload the function for different unit types, use the trait with an enable_if on the return value.

GElliott commented 6 years ago

That works well enough for me. Thanks!