Use the <random>
library instead of the obsolete and error-prone std::rand
(using the modulo operator to obtain a random integer in a range is a common mistake). See Why is the new random library better than std::rand()? for more information.
#include <iostream>
#include <random>
int main()
{
std::mt19937 engine{std::random_device{}()};
std::uniform_int_distribution<int> dist{0, 3};
switch (dist(eng)) {
case 0:
std::cout << "...\n";
break;
case 1:
std::cout << "...\n";
break;
case 2:
std::cout << "...\n";
break;
case 3:
std::cout << "...\n";
break;
}
}
Here, we first create a std::mt19937
engine, which produces uniformly distributed integers in the half-open range [0, 232), and seed it using a std::random_device
, which is supposed to generate a non-deterministic number (it may be implemented using the system time, for example). Then, we create a std::uniform_int_distribution
to map the random numbers generated by the engine to integers in the inclusive interval [0, 3] by calling it with the engine as argument.
This can be generalized by taking a range of strings to print:
template <typename RanIt, typename F>
decltype(auto) select(RanIt begin, RanIt end, F&& f)
{
if (begin == end) {
throw std::invalid_argument{"..."};
}
thread_local std::mt19937 engine{std::random_device{}()};
using index_t = long long; // for portability
std::uniforn_int_distribution<index_t> dist{0, index_t{end - begin - 1}};
return std::invoke(std::forward<F>(f), begin[dist(engine)]);
}
int main()
{
const std::array<std::string, 4> messages {
// ...
};
select(messages.begin(), messages.end(),
[](const auto& string) {
std::cout << string << '\n';
});
}
Here, we take a pair of random access iterators and a Callable object to support selecting an element from an arbitrary random-accessible range and performing an arbitrary operation on it.
First, we check if the range is empty, in which case selection is not possible and an error is reported by throw
ing an exception.
Then, we create a std::mt19937
engine that is thread_local
(that is, each thread has its own engine) to prevent data races. The state of the engine is maintained between calls, so we only seed it once for every thread.
After that, we create a std::uniform_int_distribution
to generate a random index. Note that we used long long
instead of typename std::iterator_traits<RanIt>::difference_type
: std::uniform_int_distribution
is only guaranteed to work with short
, int
, long
, long long
, unsigned short
, unsigned int
, unsigned long
, and unsigned long long
, so if difference_type
is signed char
or an extended signed integer type, it results in undefined behavior. long long
is the largest supported signed integer type, and we use braced initialization to prevent narrowing conversions.
Finally, we std::forward
the Callable object and std::invoke
it with the selected element. The decltype(auto)
specifier makes sure that the type and value category of the invocation is preserved.
We call the function with an std::array
and a lambda expression that prints the selected element.
Since C++20, we can constrain the function template using concepts:
template <std::random_access_iterator RanIt,
std::indirectly_unary_invocable<RanIt> F>
decltype(auto) select(RanIt begin, RanIt end, F&& f)
{
// ...
}
Before C++20, we can also use SFINAE:
template <typename RanIt, typename F>
std::enable_if_t<
std::is_base_of_v<
std::random_access_iterator_tag,
typename std::iterator_traits<RanIt>::iterator_category
>,
std::invoke_result_t<F, typename std::iterator_traits<RanIt>::value_type>
> select(RanIt begin, RanIt end, F&& f)
{
// ...
}