Reader small image

You're reading from  Modern C++ Programming Cookbook - Third Edition

Product typeBook
Published inFeb 2024
PublisherPackt
ISBN-139781835080542
Edition3rd Edition
Right arrow
Author (1)
Marius Bancila
Marius Bancila
author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila

Right arrow

C++ 20 Core Features

The new C++20 standard is a major step in the development of the C++ language. C++20 brings many new features both to the language and to the standard library. Some of these have already been discussed in previous chapters, such as the text formatting library, the calendar extensions to the chrono library, the changes to the thread support library, and many others. However, the features that impact the language the most are modules, concepts, coroutines, and the new ranges library. The specification of these features is very lengthy, which makes it difficult to cover them in great detail in this book. Therefore, in this chapter, we will look at the most important aspects and use cases of these features. This chapter is intended to help you start using these features.

This chapter includes the following recipes:

  • Working with modules
  • Understanding module partitions
  • Specifying requirements on template arguments with concepts
  • Using...

Working with modules

Modules are one of the most important changes in the C++20 standard. They represent a fundamental change to the C++ language and the way we write and consume code. Modules are made available in source files that are compiled separately from the translation units that consume them.

Modules provide multiple advantages, especially in comparison to the use of header files:

  • They are only imported once, and the order they’re imported in does not matter.
  • They do not require splitting interfaces and implementation in different source files, although this is still possible.
  • Modules have the potential to reduce compilation time, in some cases significantly. The entities exported from a module are described in a binary file that the compiler can process faster than traditional precompiled headers.
  • Moreover, this file can potentially be used to build integrations and interoperability with C++ code from other languages.

In this...

Understanding module partitions

The source code of a module may become large and difficult to maintain. Moreover, a module may be composed of logically separate parts. To help with scenarios such as these, modules support composition from parts called partitions. A module unit that is a partition that exports entities is called a module interface partition.

However, there could also be internal partitions that do not export anything. Such a partition unit is called a module implementation partition. In this recipe, you will learn how to work with interface and implementation partitions.

Getting ready

You should read the previous recipe, Working with modules, before continuing with this one. You will need both the module fundamentals we discussed there and the code examples that we will continue with in this recipe.

In the following examples, we will use the std module, which is only available in C++23. For previous versions, use std.core in VC++ or other particular...

Specifying requirements on template arguments with concepts

Template metaprogramming is an important part of the C++ language, empowering the development of general-purpose libraries, including the standard library. However, template metaprogramming is not trivial. On the contrary, complex tasks could be tedious and difficult to get right without a lot of experience. In fact, the C++ Core Guidelines, an initiative created by Bjarne Stroustrup and Herb Sutter, have a rule called Use template metaprogramming only when you really need to, which reasons that:

Template metaprogramming is hard to get right, slows down compilation, and is often very hard to maintain.

An important aspect concerning template metaprogramming has been the specification of constraints for type template parameters, in order to impose restrictions on the types that a template can be instantiated with. The C++20 concepts library is designed to solve this problem. A concept is a named set of constraints...

Using requires expressions and clauses

In the previous recipe, we introduced the topic of concepts and constraints, learning about them with the help of several examples that were solely based on already existing type traits. Moreover, we also used the terser syntax to specify concepts, with the concept name used instead of the typename or the class keyword in the template declaration. However, it is possible to define more complex concepts with the help of requires expressions. These are prvalues of the type bool that describe the constraints on some template arguments.

In this recipe, we will learn how to write requires expressions and an alternative way to specify constraints on template arguments.

Getting ready

The class template NumericalValue<T> and the function template wrap() defined in the previous recipe will be used in the code snippets presented in this recipe.

How to do it...

To specify requirements for template arguments, you can use requires...

Exploring abbreviated function templates

In Chapter 3, we learned about function templates as well as lambda expressions, including generic and template lambdas. A generic lambda is a lambda expression that uses auto specified for one of its parameters. The result is a function object with a templated call operator. The same is produced as a result of defining a lambda template that has the advantage, over generic lambdas, of allowing better control over the types of the parameters. In C++20, this idea of using the auto specifier for parameter types is generalized for all functions.

This introduces a simplified syntax of defining function templates, and functions that are defined in this way are called abbreviated function templates. We will see in this recipe how to use them.

How to do it…

You can define the following categories of abbreviated function templates in C++20:

  • Unconstrained abbreviated function templates that use the auto specifier to define...

Iterating over collections with the ranges library

The C++ standard library provides three important pillars—containers, iterators, and algorithms—that enable us to work with collections. Because these algorithms are for general purposes and are designed to work with iterators, which define a range, they often require writing explicit and sometimes complex code to achieve simple tasks. The C++20 ranges library has been designed to solve this problem by providing components for handling ranges of elements. These components include range adapters (or views) and constrained algorithms that work with a range instead of iterators. In this recipe, we will look at some of these views and algorithms and see how they can simplify coding.

Getting ready

In the following snippets, we will refer to a function called is_prime(), which takes an integer and returns a Boolean, indicating whether the number is prime or not. A simple implementation is shown here:

bool is_prime...

Exploring the standard range adaptors

In the previous recipe, we looked at how the ranges library helps us simplify various tasks when working with collections (ranges), such as enumerating, filtering, transforming, and reversing. We did so with the help of range adaptors. However, we have only looked at a small set of adaptors. There are more available in the standard library, some included in C++20 and others in C++23. In this recipe, we will explore all the adapters in the standard library.

Getting ready

In the snippets shown in this recipe, we will use the following namespace aliases:

namespace rv = std::ranges::views;
namespace rg = std::ranges;

Also, to compile the snippets below, you need to include the <ranges> and <algorithm> headers (for the ranges library).

How to do it…

In C++20, the following adaptors are available to use:

  • ranges::filter_view / views::filter represents a view of the underlying sequence but without...

Converting a range to a container

The result of applying various range adapters to a range (such as a container) is a complex type that is difficult to type or remember. Typically, we’d use the auto specifier to indicate the type of the result of chaining adaptors, as we saw in the previous recipes. Ranges are lazy, which means they are evaluated, and they produce results only when we iterate over them. However, we often need to store the result of applying one or more range adaptors in a container, such as a vector or a map. Prior to C++23, this required explicit coding. However, C++23 provides a range conversion function, called std::ranges::to, which makes this an easy task. It also enables conversion between different containers. In this recipe, we will learn how to use it.

Getting ready

The is_prime() function used in the following snippets was shown in the recipe Exploring the standard range adaptors and will not be listed again here.

How to do it…

...

Creating your own range view

The C++20 ranges library simplifies the handling of ranges of elements. The 16 range adaptors (views) defined in the library provide useful operations, as seen in the previous recipe. However, you can create your own view that can be used together with the standard ones. In this recipe, you will learn how to do that. We will create a view called trim that, given a range and a unary predicate, returns a new range without the front and back elements that satisfy the predicate.

Getting ready

In this recipe, we will use the same namespace aliases used in the previous one, with rg as an alias for std::ranges and rv as an alias for std::ranges::views.

How to do it...

To create a view, do the following:

  • Create a class template, called trim_view, derived from std::ranges::view_interface:
    template<rg::input_range R, typename P>
        requires rg::view<R>
    class trim_view :
        public rg::view_interface<trim_view<...

Using constrained algorithms

The C++ standard library features over 100 generic-purpose algorithms (most of them in the <algorithm> header, and some of them in the <numeric> header). Some of these algorithms we saw in Chapter 5, over the course of several recipes, when we learned how to search elements in a range, sort a range, initialize a range, and more. The generic aspect of algorithms comes from the fact they work with iterators (a beginning and end iterator to a sequence of elements–a range) but this has the downside of requiring more explicit code that needs to be repeated over and over again. To ease the use of these algorithms, the C++20 standard provides matching algorithms in the std::ranges namespace that work with ranges (but also have overloads for iterators). These algorithms from the ranges library are called constrained algorithms and are available in the <algorithm> header. Although it’s not possible to look at all of them here, in...

Creating a coroutine task type for asynchronous computations

A major component of the C++20 standard is represented by coroutines. Simply put, coroutines are functions that can be suspended and resumed. Coroutines are an alternative to writing asynchronous code. They help simplify asynchronous I/O code, lazy computations, or event-driven applications. When a coroutine is suspended, the execution returns to the caller, and the data necessary to resume the coroutine is stored separately from the stack. For this reason, the C++20 coroutines are called stackless. Unfortunately, the C++20 standard does not define actual coroutine types and only a framework for building them. This makes writing asynchronous code with coroutines difficult without relying on third-party components.

In this recipe, you will learn how to write a coroutine task type that represents an asynchronous computation, which starts executing when the task is awaited.

Getting ready

The several standard library...

Creating a coroutine generator type for sequences of values

In the previous recipe, we saw how to create a coroutine task that enables asynchronous computations. We used the co_await operator to suspend execution until resumed and the co_return keyword to complete execution and return a value. However, another keyword, co_yield, also defines a function as a coroutine. It suspends the execution of the coroutine and returns a value. It enables a coroutine to return multiple values, one each time it is resumed. To support this feature, another type of coroutine is required. This type is called a generator. Conceptually, it’s like a stream that produces a sequence of values of type T in a lazy manner (when iterated). In this recipe, we will see how we can implement a simple generator.

Getting ready

The goal of this recipe is to create a generator coroutine type that enables us to write code like the following:

generator<int> iota(int start = 0, int step = 1) noexcept...

Generating a sequence of values with the std::generator type

The C++20 standard includes two major updates to the standard library: the ranges library and coroutines. However, with regard to the latter, the support is minimal. The C++20 standard only defines a framework for building coroutines. Because of this, libraries such as libcoro, which we have previously seen, were created to provide actual coroutines, such as task and generator, which we also saw in the previous two recipes. The C++23 standard introduces the first standard coroutine, called std::generator. This brings together ranges and coroutines because std::generator is a view that represents a synchronous coroutine generator. This is a standard implementation for what we explicitly built in the previous recipe, Creating a coroutine generator type for sequences of values. Let’s see how it works.

At the time of writing, only GCC 14 supports this standard coroutine.

How to do it…

To...

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Modern C++ Programming Cookbook - Third Edition
Published in: Feb 2024Publisher: PacktISBN-13: 9781835080542
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila