Summary: Given a numeric type T, is there a concise way to declare a variable as either std::uniform_int_distribution<T>
or std::uniform_real_distribution<T>
depending upon whether T is integral or floating point?
I needed to produce random std::chrono::duration
s uniformly distributed over a caller-defined range, so I created a uniform_duration_distribution
class template modeled after that standard library's distribution class templates.
First I wrote a concept to constrain my distribution to a chrono duration (or suitably similar type).
// Is T similar enough to std::chrono::duration for our purposes?
template <typename T>
concept chrono_duration = requires (T d) {
{ d.count() } -> std::same_as<typename T::rep>;
{ d.zero() } -> std::same_as<T>;
{ d.max() } -> std::same_as<T>;
};
A duration has a numeric representation called a count. My class contains a numeric uniform distribution from the standard library, uses it to generate a count, and constructs a duration from that count.
template<chrono_duration DurationT>
class uniform_duration_distribution {
// ...
private:
using rep = typename DurationT::rep;
std::uniform_distribution<rep> m_distribution; // Whoops!
};
And therein lies the problem. The type of the duration's count can be either an integral type or a floating point type, so the type of m_distribution
isn't as simple as std::uniform_distribution<T>
because there is no such template.
I didn't want to make several specializations of my class, and I didn't want limit callers to one specific instantiation of a duraiton. I just wanted to choose the type for the contained distribution based on the duration's rep type.
My first attempt was to use a type alias template restricted by concepts.
template <std::integral IntT>
using dist_selector = std::uniform_int_distribution<IntT>;
template <std::floating_point FloatT>
using dist_selector = std::uniform_real_distribution<FloatT>;
This doesn't seem to be allowed. I can (apparently) constrain a single using alias template with a concept, but I cannot use concepts to select between different aliases. At least, not as I tried it. Is there a way to do so?
I also learned I cannot specialize using alias templates.
In the end I made a struct template with specializations for the numeric types.
// Select the appropriate distribution type based on the value type.
template <typename T> struct dist_selector {};
template <> struct dist_selector<long double> { using t = std::uniform_real_distribution<long double>; };
template <> struct dist_selector<double> { using t = std::uniform_real_distribution<double>; };
template <> struct dist_selector<float> { using t = std::uniform_real_distribution<float>; };
template <> struct dist_selector<long long> { using t = std::uniform_int_distribution<long long>; };
template <> struct dist_selector<long> { using t = std::uniform_int_distribution<long>; };
template <> struct dist_selector<int> { using t = std::uniform_int_distribution<int>; };
template <> struct dist_selector<short> { using t = std::uniform_int_distribution<short>; };
template <> struct dist_selector<unsigned long long> { using t = std::uniform_int_distribution<unsigned long long>; };
template <> struct dist_selector<unsigned long> { using t = std::uniform_int_distribution<unsigned long>; };
// ...
Then the member variable is defined as:
using rep = typename DurationT::rep;
using dist_type = typename dist_selector<rep>::t;
dist_type m_distribution;
This works but feels like falling back to an old hack. Am I missing a more modern way to do this?
You can use a class template for the specialization via the concepts, then add the alias template for convenience:
#include <random>
#include <concepts>
#include <type_traits>
template <typename T> struct dist_selector;
template <typename T> requires std::integral<T>
struct dist_selector<T> {
using type = std::uniform_int_distribution<T>;
};
template <typename T> requires std::floating_point<T>
struct dist_selector<T> {
using type = std::uniform_real_distribution<T>;
};
template <typename T>
using dist_selector_t = dist_selector<T>::type;
int main () {
static_assert(std::is_same_v<std::uniform_int_distribution<int>,dist_selector_t<int>>);
static_assert(std::is_same_v<std::uniform_real_distribution<float>,dist_selector_t<float>>);
}
Alternatively you can use std::conditional
:
template <typename T>
using dis_sel = std::conditional_t<std::is_floating_point_v<T>,
std::type_identity<std::uniform_real_distribution<T>>,
std::type_identity<std::uniform_int_distribution<T>>>::type;
Note how std::type_identity
avoids to ask for the ::type
member alias that does not exist (eg std::type_identity<std::uniform_int_distribution<double>>
is an "ok" type, it just has no type
member alias).