Search code examples
c++templatesc++-conceptsc++-chrono

Selecting a uniform distribution type from the standard library


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::durations 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?


Solution

  • 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>>);
    }
    

    Live Demo

    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;
    

    Live Demo

    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).