Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's an alternative to traits for selecting between two different named functions?

And also ... I don't want to use function pointers, I really want to use the function itself directly (so it can be inlined or have other optimizations apply).

Suppose: I've got a template function/class that is going to compute some mathematical stuff, and the template parameter is the integral type which might be unsigned int32_t or it might be unsigned int64_t.

At some point I'm going to need random numbers, so I need a generator, and in the one case I'm going to use mt19937 and in the other mt19937_64. So the actual type names are different, but I've got to pick one and actually write it in the source code.

Obviously a traits class on the integral type would work fine (and that's what I'm doing now). But it seems to me rather heavyweight syntax-wise for this one-time use, and also somewhat non-local (w.r.t. reading the source code if you get what I mean).

Another approach would be to encapsulate the use of the generator inside some (generic) function and provide full specializations of it for my two integral types. And that actually is ok.

But: Are there other alternatives? Is there some kind of compile-time "if" or "switch" (not quite enable-if which enables/disables templates from instantiating) that I can use here? Or something else (perhaps even simpler than template metaprogramming which I'm just not seeing)?

(P.S. Please don't get hung up on mt19937 & mt19937_64 - I know those are both aliases of a type I could instantiate myself with my integral type - but I'd much rather use the standard definitions with their large set of quite magic numbers. Plus I'm not just interested in mt19937/mt19937_64 but other similar cases as well.)

Here's what the code for my traits class currently looks like:

template <class Base>
struct traits { };

template <>
struct traits<unsigned __int32>
{
    using base_t = unsigned __int32;
    static const int nbits = std::numeric_limits<base_t>::digits;
    using random_engine_t = std::mt19937;
    ...
};

template <>
struct traits<unsigned __int64>
{
    using base_t = unsigned __int64;
    static const int nbits = std::numeric_limits<base_t>::digits;
    using random_engine_t = std::mt19937_64;
    ...
};
like image 643
davidbak Avatar asked Feb 07 '23 07:02

davidbak


2 Answers

Approach #1: generate clean distributed traits types.

Some utilities:

template<class T>struct tag_t{using type=T;};
template<class T>constexpr tag_t<T> tag = {};
template<class Tag>using type=typename Tag::type;

Now, instead of traits classes, we create tagged overloads:

tag<std::mt1337> which_random_engine( tag_t<int> );
tag<std::mt1337_64> which_random_engine( tag_t<std::int64_t> );

which lets you do such overloading ... wherever.

We can use this to define a traits class:

template<class T>
using random_engine_for = type<decltype(which_random_engine(tag<T>))>;

Use:

random_engine_for<T> engine;

Approach #2:

template<class A, class B> struct zpair_t {};
template<class T, class...> struct lookup_t {};
template<class T, class A, class B, class...Ts>
struct lookup_t<T, zpair_t<A, B>, Ts...>:lookup_t<T, Ts...>{};
template<class T, class B, class...Ts>
struct lookup_t<T, zpair_t<T, B>, Ts...>:tag_t<B>{};
template<class T, class Default, class...Ts>
struct lookup_t<T, tag_t<Default>, Ts...>:tag_t<Default>{
  static_assert(sizeof(Ts...)==0, "Default must be last");
};

template<class A, class B> using kv=zpair_t<A,B>;
template<class Default> using otherwise=tag_t<Default>;
template<class T, class...KVs>
using lookup = type<lookup_t<T, KVs...>>;

using random_engine_t = 
  lookup< T,
    kv< int, std::mt19937 >,
    kv< std::int64_t, std::mt19937_64 >,
    otherwise<void> // optional
  >;

which uses some of the same utilities, and does a compile-time type map.

I'm sure boost has better variation on the syntax.

like image 58
Yakk - Adam Nevraumont Avatar answered Feb 16 '23 03:02

Yakk - Adam Nevraumont


You could use std::conditional:

template <class Int>
using engine_t = std::conditional_t<
    std::is_same<Int, uint32_t>{},
    std::mt19937,
    std::mt19937_64
>;

Assuming that Int can only be uint32_t or uint64_t. This gets steadily more complicated the more types you end up with. Also it has safety issues - what if now uint16_t is supported? You'd end up silently using mt19937_64 which is probably not the right decision.


You could also use the mpl::map approach:

using engine_map = mpl::map<
    mpl::pair<uint32_t, std::mt19937>,
    mpl::pair<uint64_t, std::mt19937_64>
>;

template <class Int>
using engine_t = typename mpl::at<engine_map, Int>::type;

This extends better to more types. Also would be a hard compile error if you introduce a new type, which is probably a better approach.


Whether this is better or worse than a traits class is I think a matter of preference and depends on the rest of your project.

like image 42
Barry Avatar answered Feb 16 '23 04:02

Barry