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;
...
};
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With