Exploring Different Ways to Generate Random Numbers in C++: Coroutines and Concurrency

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.

Photo by Danny Meneses on Pexels.com

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::future containing a vector of generated random numbers.
  • C: std::async is 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::future containing the random numbers.
  • L: Wait for the std::future to 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.

Posted in Uncategorized | Leave a comment

Ranges in C++

Our book, Learning C++, has aWhen ranges are used, it makes coding easier and simper to develop and to understand, even though any code that uses range, can be also coded with an old style for..loop.

Here is an example:

#include <iostream>
#include <vector>
#include <ranges>
#include <numeric>

int main()
{
    std::vector<int> nums = { 0, 1, 2, 3, 4, 5 };

    auto even = [](int i) { return i % 2 == 0; };
    auto square = [](int i) { return i * i; };

    auto result = std::ranges::views::filter(nums, even)
        | std::ranges::views::transform(square)
        | std::ranges::views::all;

    int sum = std::accumulate(result.begin(), result.end(), 0);

    std::cout << "The sum of squared even numbers is: " << sum << std::endl;

    return 0;
}

So what do we have here? our code snippet demonstrates the use of range-based algorithms in C++ 20. In our code, std::ranges::views::filter algorithm takes the vector nums and applies the lambda function even to it to filter out only the even integers. The std::ranges::views::transform algorithm then applies the lambda function square to each element of the filtered range to square them. The resulting range is then converted into a view that includes all the elements using std::ranges::views::all.

The std::accumulate algorithm then takes the resulting view and computes the sum of all the elements in it. Finally, the sum is then printed.

You can read more about ranges in our book.

As you can see, ranges provide a more concise and expressive way to manipulate sequences, using a pipeline-like syntax. The pipeline-like syntax allows you to chain together multiple operations that operate on a sequence of elements, creating a sequence of intermediate results until a final result is produced.

Further more, ranges also allow for the creation of views. A view is a lightweight object that represents a view of a sequence of elements, without actually copying the elements themselves. Views allow you to create lazy evaluation, meaning that the elements are evaluated only when needed, which can save time and memory.

One of the key benefits of ranges is that they simplify code, making it easier to read, write, and understand. They provide a more expressive syntax than the traditional approach of writing for-loops and calling algorithms. Ranges also provide compile-time safety, reducing the risk of errors and making it easier to debug code.

Another advantage of ranges is that they can improve performance. By creating views, it’s possible to defer the computation of elements until they are actually needed, which can reduce the number of intermediate results and the overall memory usage. Ranges also make it easier to write code that can take advantage of parallelism.

Posted in Uncategorized | Leave a comment

Why do we need modules

I have been using C++ since 1996. Yes, C++ is an old language but I always found it the best language to u to achieve any goal, including goals that are typically associated with other languages. I use C++ for automation (and not Python) for example. In our book, we also try to include unconventional abilities and capabilities done in C++, showing how C++ can be use for almost anything.

One of the biggest problems with C++ is its header file system.

You have to include a lot of headers to use even the simplest libraries, and this can lead to long compile times, conflicts, and other issues.

That’s where C++ 20 modules come in – a new feature that aims to make our lives as C++ developers easier. But what are modules, and why do we need them? Let’s find out.

What are modules

Modules are a new way of organizing C++ code that allows us to encapsulate code within a module and export only the symbols we want to make public. This means that we can avoid naming conflicts, manage dependencies between different parts of our codebase, and reduce compilation times. Module are among the topics covered in our book Learning C++ 

How Do We Use Modules?

To use modules, we first need to create a module declaration file (.ixx) and a module implementation file (.cxx). The declaration file defines the interface of the module, while the implementation file contains the implementation details. We can then import the module in our main source file using the import statement.

module example;
export int add(int a, int b) { return a + b; }

We can then import this module in our main source file like this:

import example;
int main() 
{ 
    int sum = add(2, 3); 
    return 0; 
}

As you can see, using modules is much cleaner and easier than using headers, and it can save us a lot of time and headaches.

Why Do We Need Modules?

There are several reasons why we need modules in C++. First, modules can help us organize our code in a more modular way, making it easier to manage dependencies and reduce complexity. Second, they can help us avoid naming conflicts and reduce namespace pollution, which can lead to more readable and maintainable code. And third, they can help us reduce compilation times and improve overall performance.

Imagine a world where you don’t have to wait 10 minutes for your code to compile just because you included a few too many headers. That’s the world that modules can help us create.

What’s Next for Modules?

C++ 20 modules are just the beginning. There are already plans to add new features and improvements to the module system in future versions of C++, including support for hierarchical modules, better tooling, and more. So if you’re a C++ developer, now is the time to start learning about modules and how to use them in your code.

So to sum up, C++ 20 modules are a much-needed addition to the language that can help us write cleaner, more maintainable, and more efficient code. So if you’re tired of dealing with header files and long compile times, give modules a try and see how they can improve your development experience.

Posted in Code Snippets | Tagged , , , , , , , , | Leave a comment

Handling Exceptions

An important part of the book Learning C++  exception handling, and especially exception handling in C++ 20. Here is an example:



#include <strexcept>
#include <iostream>



class MyException : public std::runtime_error
{
public:
MyException(const char* message) : std::runtime_error(message) {}



/* a constructor for MyException that takes a message argument and passes it to the base class
constructor */
};



class MyClass
{
public:
   void foo()
   {
      throw MyException("An error occurred in MyClass::foo()");
      /* throw an instance of MyException with a custom message */
   }
};



int main()
{
   MyClass obj;
   try
   {
      obj.foo(); // call MyClass::foo() inside a try block
   }
   catch (const std::exception& e)
   {
      std::cerr << "Exception caught: " << e.what() << std::endl;
      /* catch any exception of type std::exception or its derived classes and print its error message to the
         standard error stream */
   }
   return 0;
}




Posted in Code Snippets | Leave a comment

Capitals

One of the code samples in my book Learning C++  is Capitals. The purpose of the code was to explain key terms in the C++ 20 Standard Library, along with a small twist. The program tells you the capital of a country you enter, but if it doen’t have the answer, it asks you and then add your answer to its “knowledge base”, so next time it does.

Posted in Code Snippets | Leave a comment