tl;dr: Учитывая числовой тип T, существует ли краткий способ объявить переменную как std::uniform_int_distribution<T>
или std::uniform_real_distribution<T>
в зависимости от того, является ли T целым или с плавающей запятой?
Мне нужно было создать случайные std::chrono::duration
, равномерно распределенные по диапазону, определяемому вызывающей стороной, поэтому я создал шаблон uniform_duration_distribution
класса, смоделированный по образцу шаблонов классов распределения стандартной библиотеки.
Сначала я написал концепцию, позволяющую ограничить мое распределение длительностью хрона (или аналогичным типом).
// 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>;
};
Длительность имеет числовое представление, называемое счетчиком. Мой класс содержит числовое равномерное распределение из стандартной библиотеки, использует его для генерации счетчика и строит продолжительность на основе этого счетчика.
template<chrono_duration DurationT>
class uniform_duration_distribution {
// ...
private:
using rep = typename DurationT::rep;
std::uniform_distribution<rep> m_distribution; // Whoops!
};
И в этом проблема. Тип счетчика длительности может быть либо целочисленным, либо типом с плавающей запятой, поэтому тип m_distribution
не такой простой, как std::uniform_distribution<T>
, поскольку такого шаблона не существует.
Я не хотел делать несколько специализаций своего класса и не хотел ограничивать вызывающих абонентов одним конкретным экземпляром duraiton. Я просто хотел выбрать тип автономного распределения на основе типа повторения продолжительности.
Моей первой попыткой было использовать шаблон псевдонима типа, ограниченный концепциями.
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>;
Кажется, это запрещено. Я могу (по-видимому) ограничить один шаблон использования псевдонима концепцией, но я не могу использовать концепции для выбора между различными псевдонимами. По крайней мере, не так, как я пробовал. Есть ли способ сделать это?
Я также узнал, что не могу специализироваться на использовании шаблонов псевдонимов.
В конце я создал шаблон структуры со специализацией для числовых типов.
// 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>; };
// ...
Тогда переменная-член определяется как:
using rep = typename DurationT::rep;
using dist_type = typename dist_selector<rep>::t;
dist_type m_distribution;
Это работает, но похоже на возврат к старому хаку. Мне не хватает более современного способа сделать это?
Вы можете использовать шаблон класса для специализации с помощью концепций, а затем для удобства добавить шаблон псевдонима:
#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>>);
}
Альтернативно вы можете использовать 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;
Обратите внимание, как std::type_identity
избегает запроса псевдонима участника ::type
, который не существует (например, std::type_identity<std::uniform_int_distribution<double>>
— это тип «ОК», у него просто нет псевдонима участника type
).
Ах, гибрид того, что я пробовал. Я этого не видел. Спасибо! Я еще не изучал std::type_identity
, но тоже посмотрю.
Я бы начал с признака/концепции типа, чтобы ограничить параметр шаблона типом std::chrono::duration
:
template<class>
struct is_duration : std::false_type {};
template<class Rep, class Period>
struct is_duration<std::chrono::duration<Rep, Period>> : std::true_type {};
template<class T>
concept chrono_duration = is_duration<T>::value;
Тогда селектор распределения мог бы выглядеть так, где экземпляр T
должен соответствовать одной из ограниченных функций test
:
template<class T>
struct distribution_selector {
template<class I>
requires std::integral<I>
static auto test(I) -> std::uniform_int_distribution<I>;
template<class F>
requires std::floating_point <F>
static auto test(F) -> std::uniform_real_distribution<F>;
using type = decltype(test(std::declval<T>()));
};
template<class T>
using distribution_selector_t = distribution_selector<T>::type;
Собираем это вместе:
template<chrono_duration DurationT>
class uniform_duration_distribution {
public:
using rep = DurationT::rep;
distribution_selector_t<rep> m_distribution;
};
Да, это работает, и ты научил меня вещам. Однако объявлять тестовые функции для получения типа возвращаемого значения кажется несколько окольным путем. Хотя это немного короче, я не уверен, что читателю легче понять, что происходит, по сравнению с утомительным списком специализаций, который у меня был.
Как вы собираетесь инициализировать дистрибутив? Предполагая, что это достаточно небольшой диапазон [a,b], вам нужно будет инициализировать Uniform_int_distribution с помощью a,b и Uniform_real_distribution с помощью a,nextafter(b,max). А затем при генерации для Uniform_real_distribution вам придется повторно генерировать, чтобы отбросить >b, чтобы обойти распространенную ошибку. Так что дело не только в типе. Вероятно, он заслуживает своего собственного типа распространения или, по крайней мере, типа признаков.