I have implemented a utility class to perform various random number related task. I am looking for any feedback on design / implementation. I plan to expand the class, and so would like to correct any "bad habits" and or improve the basics before I proceed.
Random.hpp
#pragma once
#include <gsl/gsl> // gsl::narrow (for safety)
#include <random>
#include <cstdint> // uintmax_t
#include <chrono> // For DefaultSeed generation
#include <iostream>
#include <type_traits>
#include <iterator> // std::distance
namespace ae
{
namespace details
{
template <typename T>
inline constexpr bool IsCharacterV
{
std::is_same_v<T, char>
|| std::is_same_v<T, signed char>
|| std::is_same_v<T, unsigned char>
|| std::is_same_v<T, wchar_t>
// || std::is_same_v<T, char8_t> C++ 20
|| std::is_same_v<T, char16_t>
|| std::is_same_v<T, char32_t>
};
template <typename T>
inline constexpr bool IsRealV{ std::is_floating_point_v<T> };
template <typename T>
inline constexpr bool IsIntegerV{ std::is_integral_v<T> && (!IsCharacterV<T>) };
class DefaultSeeder
{
using Clock = std::chrono::high_resolution_clock;
public:
auto& operator()() noexcept
{
return this->seed;
}
private:
std::seed_seq seed
{
{
Clock::now().time_since_epoch().count(),
Clock::now().time_since_epoch().count()
}
};
};
}
template
<
typename Engine,
typename Seeder
>
class BasicRandom
{
public:
////////////////////////////////////////////////////////////
// Ranges
////////////////////////////////////////////////////////////
// Integer range
template <typename T>
[[nodiscard]]
static std::enable_if_t<details::IsIntegerV<T>, T> range(T min, T max)
{
const std::uniform_int_distribution<T> distribution(min, max);
return distribution(engineInstance());
}
// Real range
template <typename T>
[[nodiscard]]
static std::enable_if_t<details::IsRealV<T>, T> range(T min, T max)
{
const std::uniform_real_distribution<T> distribution(min, max);
return distribution(engineInstance());
}
// Call integer or real range according to common_type
template <typename T, typename U>
[[nodiscard]]
static auto range(T min, U max)
{
using common_type = typename std::common_type_t<T, U>;
return range(gsl::narrow<common_type>(min), gsl::narrow<common_type>(max)); // gsl::narrow will throw if the cast changed the value of its paramter
}
////////////////////////////////////////////////////////////
// Choice(s)
////////////////////////////////////////////////////////////
template <typename T>
[[nodiscard]]
static auto choice(T first, T last) // Uses range(x, y) internally
{
const auto distance{ std::distance(first, last) };
const auto rand{ range(0, distance - 1) };
return *std::next(first, rand);
}
template <typename T>
[[nodiscard]]
static auto choice(T container) // Uses range(x, y) internally
{
return choice(container.begin(), container.end());
}
template <typename T>
[[nodiscard]]
static auto choices(T first, T last, std::size_t amount)
{
std::vector<typename std::iterator_traits<T>::value_type> results(amount);
for (auto& val : results)
{
val = choice(first, last);
}
return results;
}
template <typename T>
[[nodiscard]]
static auto choices(T container, std::size_t amount)
{
std::vector<typename T::value_type> results(amount);
for (auto& val : results)
{
val = choice(container.begin(), container.end());
}
return results;
}
////////////////////////////////////////////////////////////
// Misc
////////////////////////////////////////////////////////////
template <typename T>
[[nodiscard]]
static auto shuffle(T first, T last)
{
std::shuffle(first, last, engineInstance());
}
template <typename T>
[[nodiscard]]
static auto shuffle(T& container)
{
std::shuffle(container.begin(), container.end(), engineInstance());
}
template <typename T> // Use floating point values for T..
[[nodiscard]]
static auto chance(T p)
{
const std::bernoulli_distribution distribution(p);
return distribution(engineInstance());
}
private:
[[nodiscard]]
static Engine& engineInstance()
{
static Engine engine{ Seeder{}() };
return engine;
}
};
using Random = BasicRandom<std::mt19937_64, details::DefaultSeeder>;
}
Here is some example usage, that ive also used to make sure everything works as expected:
Main.cpp
#include "Random.hpp"
#include <iostream>
#include <numeric>
int main()
{
const auto int_range{ ae::Random::range(0, 100) };
const auto double_range{ ae::Random::range(0.0, 50.0) };
const auto uint_range{ ae::Random::range(5u, 100) }; // std::common_type will make the result unsigned int
constexpr auto chance_to_roll_6{ 1.0 / 6.0 };
constexpr auto chance_to_roll_6_twice{ chance_to_roll_6 * chance_to_roll_6 };
const auto roll_6_with_dice{ ae::Random::chance(chance_to_roll_6) };
const auto roll_6_with_dice_twice{ ae::Random::chance(chance_to_roll_6_twice) };
std::array<double, 5> my_values = { 1.6, 2.5, 1.73, 3.51, 53.21 };
const auto random_element{ ae::Random::choice(my_values) };
const auto only_first_three_elements{ ae::Random::choice(my_values.begin(), my_values.begin() + 3) };
const auto multiple_choices{ ae::Random::choices(my_values, 10) };
const auto multiple_choices_only_first_2{ ae::Random::choices(my_values.begin(), my_values.begin() + 2, 10) };
std::vector<int> my_vector(10);
std::iota(my_vector.begin(), my_vector.end(), 0);
ae::Random::shuffle(my_vector);
std::cout << int_range << std::endl;
std::cout << double_range << std::endl;
std::cout << uint_range << std::endl;
std::cout << roll_6_with_dice << std::endl;
std::cout << roll_6_with_dice_twice << std::endl;
std::cout << random_element << std::endl;
std::cout << only_first_three_elements << std::endl;
for (const auto& elem : multiple_choices) std::cout << elem << std::endl;
for (const auto& elem : multiple_choices_only_first_2) std::cout << elem << std::endl;
for (const auto& elem : my_vector) std::cout << elem << std::endl;
}