How to test if exists a template function specialization
Asked Answered
C

4

5

I'm managing units conversion. Say us that I reached a state where I achieve that.

The heart of my conversion between different units lies on the following generic template function:

template <class SrcUnit, class TgtUnit> extern
double convert(double val);

The goal of this function is to convert a physical magnitude expressed in units of type SrcUnit to another expressed in units of type TgtUnit.

I have a class called Quantity<Unit> which manages the values and their unities and this class tries to give type safety and automatic conversion. By example, I export the following constructor:

  template <class SrcUnit>
  Quantity(const Quantity<SrcUnit> & q)
    : unit(UnitName::get_instance())
  {
    check_physical_units(q); // verify that the physical magnitudes are the same
    value = convert<SrcUnit, UnitName>(q.value); // <<- here the conversion is done
    check_value(); // check if the value is between the allowed interval
  }

I export other stuff where the conversion is done.

So, when someone wishes to manage a new unit, she specifies a new Unit derived class. I achieve that by putting inside a macro all the needed specification of the new class. Now, it is the responsibility of this user to write the conversions functions. That is to write two specializations of the convert() template. By example, suppose that you have a unit called 'Kilometerand you wish to specify a new unit calledMile`. In this case, you do this:

Declare_Unit(Mile, "mi", "English unit of length", Distance,
         0, numeric_limits<double>::max()); // macro instantiating a new Unit class
template <> double convert<Kilometer, Mile>(double val) { return val/1609.344; }
template <> double convert<Mile, Kilometer>(double val) { return 1609.344*val; }

Now, what happen if the user forgets to write a conversion function? Well, in this case the linker will fail because it cannot find the convert() specialization.

Now my question.

Although I think a linker error is acceptable as behavior for reporting to the user the missing convert(), I would like to test i compiling time for the existence of convert() specialization. So my question is how could I achieve that? I guess through a static_assert put just before each call to convert() which tests if the specialization is already known. But how to do that?

PS: Also, it would be very useful for me if someone could recommend me a good text about C++ metaprogramming.

Cattle answered 28/7, 2016 at 13:21 Comment(5)
Check link. There authors are developing a unit conversion system using metaprogramming machinery. Overall, the best TMP introduction imho.Diminished
This seems like a fundamentally bad design. So you have N^2 conversion functions? You have to implement both Kilometer to Mile and Mile to Kilometer separately?Oceania
If the physics would allow a N^2 conversion, then yes @Barry. And for the second question yes too. Note that there are cases where the conversion is not strictly linear; although this could sound bizarre, this is the case for some geological correlations where a conversion could be valid only inside an empirically valid interval. Inclusive, there are situations in which the conversion is only allowed in once sense. Anyway, any suggestion bout design is very welcomeCattle
What I mean by N^2 is that if you have N units, then each unit needs its own conversion function to every other unit... so you have N^2 different conversion functions. That... doesn't scale very well.Oceania
okay @Oceania I understand. And yes, that does not scale very well, but I believe there is no escape. Some other systems that I examined use a matrix conversion, which although it could be more concise is still quadratic. In the field in which I am currently working, is impressive the babel of empirical formulas and the use of different units. RegardsCattle
C
4

You could do it by placing a static_assert in the primary function template with a little trick that will assure that your program is not ill formed, stolen shamelessly from here :

template <typename...> struct always_false : std::false_type {};

template<typename T, typename U> double convert(double) {
  static_assert(always_false<T, U>::value, "No specialization exists!");
}

template <> double convert<Kilometer, Mile>(double val) { return val/1609.344; }
template <> double convert<Mile, Kilometer>(double val) { return 1609.344*val; }

Live Demo

Cilla answered 28/7, 2016 at 13:27 Comment(6)
static_assert(false,...); is ill-formed.Shakedown
Your initial foo() is ill-formed as the static_assert() doesn't depend on T.Oceania
@Shakedown is this still ill-formed?Cilla
@Oceania is this still ill-formed?Cilla
@101010 That's fine :)Shakedown
I just finished my little library. I used your approach. Thanks to you and to all who were involvedCattle
F
4

You shall not promise to compiler that for every possible combination of SrcUnit/TgtUnit there exist a conversion:

template <class SrcUnit, class TgtUnit>
double convert(double val);

Instead you should do otherwise, delete generic converter:

template <class SrcUnit, class TgtUnit>
double convert(double val) = delete;

And for every conversion that you are providing - you should forward declare this in your header file/files:

template <>
double convert<Mille,Km>(double val);
template <>
double convert<Milliseconds,Hours>(double val);

So, in this way it will be known to compiler what is provided, what is not


Aside note: your original solution might also be in danger of violating One Definition Rule. Linker was not able to detect if you mistakenly provide two different implementation of one conversion.

Flagstaff answered 28/7, 2016 at 13:27 Comment(0)
C
4

I would approach this with tags.

template<class T>struct tag_t{constexpr tag_t(){}; using type=T;};
template<class T>constexpr tag_t<T> tag{};

this lets you pass types around as function parameters.

Now, instead of using specialization and the like, we just call a function.

We'll use a better name than convert, like unit_convert.

namespace my_ns {
  Declare_Unit(Mile, "mi", "English unit of length", Distance,
     0, numeric_limits<double>::max()); // macro instantiating a new Unit 
  inline double unit_convert( tag_t<Meter>, tag_t<Mile>, double val ) {
    return val/1609.344
  }
  inline double unit_convert( tag_t<Mile>, tag_t<Meter>, double val ) {
    return val*1609.344
  }
}

Next we write a trait that says if unit_convert( tag<A>, tag<B>, double ) exists:

namespace details {
  template<template<class...>class Z, class=void, class...Ts>
  struct can_apply:std::false_type{};
  template<template<class...>class Z,  class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z,void,Ts...>;

template<class...Args>
using convert_unit_result = decltype( convert_unit( std::declval<Args>()... ) );

template<class SourceUnit, class DestUnit>
using can_convert_units = can_apply< convert_unit_result, tag_t<SourceUnit>, tag_t<DestUnit>, double >;

and now can_convert_units<Mile,Meter> is std::true_type, while can_conver_units<Chicken, Egg> is std::false_type (assuming chicken and egg are types that are not convertible units, naturally).

As a bonus, this technique permits you to put your units in individual namespaces, and define their conversion functions there as well. If both Mile and Meter attempt to define conversion between them, you'll get errors when you try to convert (ambiguous). The conversion function can be in either namespace, not both.

Cardiogram answered 28/7, 2016 at 14:11 Comment(0)
F
0

Maybe this can help.

template<typename Src, typename Tgt, typename Exists = void>
double convert (double val) {
    static_assert(not std::is_void<Exists>::value, " err msg");
}

template <> double convert<Kilometer, Mile>(double val) { return val/1609.344; }
F answered 28/7, 2016 at 14:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.