C++ is a versatile and powerful programming language that offers a variety of ways to solve problems.
In the more advanced chapters of our book, Learning C++, we explore two different approaches to generating random numbers using C++20 coroutines and concurrency features (which was added to C++ must earlier, as part of C++ 11).
Each method has its unique advantages, and by examining them, we can better understand how C++ enables developers to choose the best practice for their specific needs.

Version 1: Using C++20 Coroutines
The first version of our random number generator uses C++20 coroutines to create a generator that produces random numbers. Coroutines are a new feature introduced in C++20 that allows functions to have their execution suspended and later resumed, enabling asynchronous and non-blocking execution.
The generator can be consumed incrementally, generating random numbers as needed. Our generator is used for our final chapter of the book, which has the final book’s project.
Here’s the complete code for this version:
#include <iostream>
#include <random>
#include <coroutine>
#include <concepts>
template <std::integral T> // #A
struct generator
{
struct promise_type; // #B
using handle_type = std::coroutine_handle<promise_type>; // #C
struct promise_type
{
T value; // #D
generator get_return_object() // #E
{
return generator{ handle_type::from_promise(*this) };
}
std::suspend_always initial_suspend() // #F
{
return {};
}
std::suspend_always final_suspend() noexcept // #G
{
return {};
}
std::suspend_always yield_value(T value_) // #H
{
value = value_;
return {};
}
void return_void() // #I
{
}
void unhandled_exception() // #J
{
}
};
explicit generator(handle_type handle) : handle_(handle) // #K
{
}
~generator() // #L
{
if (handle_)
handle_.destroy();
}
generator(const generator&) = delete; // #M
generator& operator=(const generator&) = delete; // #N
generator(generator&& other) noexcept : handle_(other.handle_) // #O
{
other.handle_ = {};
}
generator& operator=(generator&& t) noexcept // #P
{
if (this == &t) // #P1
return *this;
if (handle_) // #P2
handle_.destroy();
handle_ = t.handle_; // #P3
t.handle_ = {}; // #P4
return *this; // #P5
}
int operator()() // #Q
{
handle_.resume();
return (handle_.promise().value);
}
[[nodiscard]] bool done() const noexcept // #R
{
return (handle_.done());
}
private:
handle_type handle_; // #S
};
generator<int> random_numbers(int count, int min, int max) // #T
{
std::random_device rd; // #T1
std::mt19937 gen(rd()); // #T2
std::uniform_int_distribution<> dist(min, max); // #T3
for (int i = 0; i < count; ++i) // #T4
{
co_yield dist(gen); // #T5
}
}
int main()
{
int count = 10;
int min = 1;
int max = 100;
generator<int> random_nums = random_numbers(count, min, max); // #U
while (!random_nums.done()) // #V
{
std::cout << random_nums() << " "; // #W
}
return 0;
}
- A: Declare a generator struct template that works with integral types.
- B: Forward declare the promise_type nested struct.
- C: Define a type alias for the coroutine handle with the promise_type.
- D: Store the current value of the generator.
- E: Return a generator instance when the coroutine starts.
- F: Suspend the coroutine initially and return to the caller.
- G: Suspend the coroutine when it is about to finish.
- H: Suspend the coroutine while yielding a value.
- I: Return nothing from the generator when the coroutine finishes.
- J: Handle any unhandled exceptions in the coroutine.
- K: Constructor for the generator struct taking a coroutine handle.
- L: Destructor for the generator struct, destroying the coroutine handle.
- M: Delete the copy constructor to make the generator move-only.
- N: Delete the copy assignment operator to make the generator move-only.
- O: Move constructor for the generator struct.
- P: Move assignment operator for the generator struct.
- P1: Check if the object is being assigned to itself.
- P2: Destroy the current coroutine handle if it is valid.
- P3: Move the coroutine handle from the other object.
- P4: Set the other object’s handle to an empty state.
- P5: Return a reference to the current object.
- Q: Resume the coroutine and return the value yielded.
- R: Check if the coroutine is done executing.
- S: Store the coroutine handle.
- T: Define a generator function that yields random numbers.
- T1: Create a random device for seeding the random number generator.
- T2: Create a Mersenne Twister random number generator with the random device seed.
- T3: Create a uniform integer distribution with the provided min and max values.
- T4: Loop to generate the specified number of random numbers.
- T5: Yield a random number from the generator using the distribution.
- U: Create a generator instance with the random_numbers function.
- V: Loop while the generator is not done executing.
- W: Output the generated random number.
Version 2: Using C++11 Concurrency
The second version of our random number generator uses C++11 concurrency features, specifically std::future and std::async. This approach generates random numbers asynchronously, allowing for concurrent execution of other tasks while the random numbers are being generated. Here’s the complete code for this version:
#include <iostream>
#include <random>
#include <future>
#include <vector>
#include <concepts>
template <std::integral T> // #A
std::future<std::vector<T>> random_numbers(int count, int min, int max) // #B
{
return std::async([=]() // #C
{
std::vector<T> result(count); // #D
std::random_device rd; // #E
std::mt19937 gen(rd()); // #F
std::uniform_int_distribution<> dist(min, max); // #G
for (int i = 0; i < count; ++i) // #H
{
result[i] = dist(gen); // #I
}
return result; // #J
});
}
int main()
{
int count = 10;
int min = 1;
int max = 100;
std::future<std::vector<int>> random_nums_future = random_numbers<int>(count, min, max); // #K
std::vector<int> random_nums = random_nums_future.get(); // #L
for (int num : random_nums) // #M
{
std::cout << num << " "; // #N
}
return 0;
}
- A: Using C++20 concepts to ensure the function works only with integral types.
- B: This function returns a
std::futurecontaining a vector of generated random numbers. - C:
std::asyncis used to run the lambda function asynchronously. - D: The result vector, which will store the generated random numbers.
- E: Random device to generate a seed for the random number generator.
- F: Mersenne Twister random number generator.
- G: Uniform integer distribution with the specified range.
- H: Loop to generate the required number of random numbers.
- I: Generate random numbers and store them in the result vector.
- J: Return the result vector, which will be stored in the
std::future. - K: Create a
std::futurecontaining the random numbers. - L: Wait for the
std::futureto complete and obtain the vector of random numbers. - M: Iterate through the vector of random numbers.
- N: Print each random number.
In this version, we define a function template random_numbers that returns a std::future containing a vector of generated random numbers. The function uses std::async to run a lambda function that generates random numbers and stores them in a vector. The main function creates a std::future containing the random numbers, waits for the future to complete, and then obtains the vector of random numbers.
Conclusion
C++ provides various ways to generate random numbers, allowing developers to choose the best practice depending on the specific requirements of their application. In this blog post, we explored two different approaches using C++20 coroutines and C++11 concurrency features. The coroutine-based version is suitable for situations where asynchronous data generation is needed, or when the random number sequence must be consumed as it is generated. On the other hand, the concurrency-based version leverages std::future and std::async to perform concurrent execution of tasks, potentially improving the overall performance of the program.
Ultimately, the choice between these two approaches depends on the unique needs and constraints of your application.